mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Compare commits
2 Commits
65ee223876
...
21b100b789
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b100b789 | ||
|
|
89f1804184 |
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
Two conventions coexist; which one to use depends on the project layer:
|
Two conventions coexist; which one to use depends on the project layer:
|
||||||
|
|
||||||
- **C code** (TactilityKernel, drivers): `lower_snake_case` for files, functions, variables. `UpperCamelCase` for types. Files in `source/`, `include/`, `private/` directories.
|
- **C code**: `lower_snake_case` for files, functions, variables. `UpperCamelCase` for types. Files in `source/`, `include/`, `private/` directories.
|
||||||
- **C++ code** (Tactility, apps, services): `UpperCamelCase` for files and types. `lowerCamelCase` for functions. Files in `Source/`, `Include/`, `Private/` directories.
|
- **C++ code**: `UpperCamelCase` for files and types. `lowerCamelCase` for functions. Files in `Source/`, `Include/`, `Private/` directories.
|
||||||
|
|
||||||
For projects that emit C headers and have a C++ implementation file: the internal C++ function naming should be snake_case.
|
For projects that emit C headers and have a C++ implementation file: the internal C++ function naming should be snake_case.
|
||||||
|
|
||||||
@ -11,24 +11,10 @@ Formatting is enforced by `.clang-format` (LLVM-based, 4-space indent, no column
|
|||||||
Never throw exceptions — use return types for error handling. Use `enum class` over plain `enum` when writing C++ code.
|
Never throw exceptions — use return types for error handling. Use `enum class` over plain `enum` when writing C++ code.
|
||||||
Do not add redundant null checks for parameters with an explicit non-null precondition.
|
Do not add redundant null checks for parameters with an explicit non-null precondition.
|
||||||
|
|
||||||
Code Comments:
|
Code Comments (important!):
|
||||||
|
|
||||||
- Should be as short as possible, leaving only important context.
|
- Should be minimal, but must contain critical information.
|
||||||
- Should avoid explaining what the code does, unless the code complexity is high enough to warrant an explanation.
|
- Must not explain how code was before, or how it was changed.
|
||||||
- Must avoid explaining how the code was before, or how it was changed.
|
|
||||||
- Should explain why code is implemented.
|
|
||||||
- Should be as brief as possible without losing critical information.
|
|
||||||
- Should avoid explaining what was not implemented.
|
- Should avoid explaining what was not implemented.
|
||||||
- Should avoid referring to designs of other subsystems.
|
- Must avoid referring to designs of other subsystems.
|
||||||
- Must avoid interjections: avoid hyphens or braces to interject. If interjections provide crucial info, use Doxygen entity/anchor references like:
|
- Must avoid interjections: avoid hyphens or braces to interject.
|
||||||
/**
|
|
||||||
* A dedicated completion \signal for one app instance's task.
|
|
||||||
* Whichever \side finishes with it last is the one that deletes `semaphore` and frees this struct.
|
|
||||||
*
|
|
||||||
* \signal Not the task's shared default FreeRTOS notification, which app_event.cpp's AppEventSubscription also uses.
|
|
||||||
* An unrelated event delivered to the same task could otherwise unblock a waiter early.
|
|
||||||
* \side The exiting task or a concurrent app_scheduler_stop() that found the entry in time and is waiting on `semaphore`.
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
.github/actions/build-simulator/action.yml
vendored
14
.github/actions/build-simulator/action.yml
vendored
@ -4,11 +4,8 @@ inputs:
|
|||||||
os_name:
|
os_name:
|
||||||
description: A descriptive name for the operating system (e.g. linux, windows)
|
description: A descriptive name for the operating system (e.g. linux, windows)
|
||||||
required: true
|
required: true
|
||||||
platform_name:
|
architecture:
|
||||||
description: A descriptive name for the target platform (e.g. amd64, aarch64, etc.)
|
description: A descriptive name for the target architecture (e.g. x86_64, aarch64, etc.)
|
||||||
required: true
|
|
||||||
publish:
|
|
||||||
description: A boolean that enables publishing of artifacts
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
@ -55,11 +52,10 @@ runs:
|
|||||||
run: cmake --build buildsim --target Tactility
|
run: cmake --build buildsim --target Tactility
|
||||||
- name: 'Release'
|
- name: 'Release'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: Buildscripts/release-simulator.sh buildsim release/Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }}
|
run: Buildscripts/release-simulator.sh buildsim release/Simulator-${{ inputs.os_name }}-${{ inputs.architecture }}
|
||||||
- name: 'Upload Artifact'
|
- name: 'Upload Artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ inputs.publish == 'true' }}
|
|
||||||
with:
|
with:
|
||||||
name: Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }}
|
name: Simulator-${{ inputs.os_name }}-${{ inputs.architecture }}
|
||||||
path: release/Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }}
|
path: release/Simulator-${{ inputs.os_name }}-${{ inputs.architecture }}
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|||||||
7
.github/workflows/build-simulator.yml
vendored
7
.github/workflows/build-simulator.yml
vendored
@ -17,8 +17,7 @@ jobs:
|
|||||||
uses: ./.github/actions/build-simulator
|
uses: ./.github/actions/build-simulator
|
||||||
with:
|
with:
|
||||||
os_name: linux
|
os_name: linux
|
||||||
platform_name: amd64
|
architecture: x86_64
|
||||||
publish: true
|
|
||||||
macOS:
|
macOS:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
@ -29,6 +28,4 @@ jobs:
|
|||||||
uses: ./.github/actions/build-simulator
|
uses: ./.github/actions/build-simulator
|
||||||
with:
|
with:
|
||||||
os_name: macos
|
os_name: macos
|
||||||
platform_name: aarch64
|
architecture: aarch64
|
||||||
# macOS simulator currently fails due to main thread requirement for rendering
|
|
||||||
publish: false
|
|
||||||
|
|||||||
@ -62,9 +62,13 @@ if (DEFINED ENV{ESP_IDF_VERSION})
|
|||||||
# to every target; PanicHandler.cpp branches internally per architecture.
|
# to every target; PanicHandler.cpp branches internally per architecture.
|
||||||
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND)
|
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND)
|
||||||
|
|
||||||
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=read" APPEND)
|
# Wraps newlib's reentrant syscall stubs, not the plain read()/write()/close() newlib itself
|
||||||
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=write" APPEND)
|
# implements as thin wrappers around them. newlib's own stdio (fflush()'s buffer-flush path in particular)
|
||||||
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=close" APPEND)
|
# calls these _r stubs directly, bypassing the plain names entirely.
|
||||||
|
# See Modules/app-module/source/stdio_wrap.cpp's own comment for the exact call chain.
|
||||||
|
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_read_r" APPEND)
|
||||||
|
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_write_r" APPEND)
|
||||||
|
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_close_r" APPEND)
|
||||||
|
|
||||||
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_button_create" APPEND)
|
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_button_create" APPEND)
|
||||||
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_dropdown_create" APPEND)
|
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_dropdown_create" APPEND)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ constexpr auto* TAG = "Tab5";
|
|||||||
// Mirrors device_construct_add_start(), but with a device_set_parent() call inserted between
|
// Mirrors device_construct_add_start(), but with a device_set_parent() call inserted between
|
||||||
// construct and add - device_set_parent() asserts on device->internal (only valid after
|
// construct and add - device_set_parent() asserts on device->internal (only valid after
|
||||||
// construct), and device_construct_add_start() doesn't expose a hook to call it before add().
|
// construct), and device_construct_add_start() doesn't expose a hook to call it before add().
|
||||||
bool construct_add_start(Device* device, Device* parent, const char* compatible) {
|
bool construct_add(Device* device, Device* parent, const char* compatible) {
|
||||||
error_t error = device_construct(device);
|
error_t error = device_construct(device);
|
||||||
if (error != ERROR_NONE) {
|
if (error != ERROR_NONE) {
|
||||||
LOG_E(TAG, "display_detect: failed to construct %s: %s", device->name, error_to_string(error));
|
LOG_E(TAG, "display_detect: failed to construct %s: %s", device->name, error_to_string(error));
|
||||||
@ -33,7 +33,15 @@ bool construct_add_start(Device* device, Device* parent, const char* compatible)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
error = device_start(device);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool construct_add_start(Device* device, Device* parent, const char* compatible) {
|
||||||
|
if (!construct_add(device, parent, compatible)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t error = device_start(device);
|
||||||
if (error != ERROR_NONE) {
|
if (error != ERROR_NONE) {
|
||||||
LOG_E(TAG, "display_detect: failed to start %s: %s", device->name, error_to_string(error));
|
LOG_E(TAG, "display_detect: failed to start %s: %s", device->name, error_to_string(error));
|
||||||
device_remove(device);
|
device_remove(device);
|
||||||
|
|||||||
@ -10,6 +10,10 @@
|
|||||||
// device_construct_add_start(), but with a device_set_parent() call inserted between construct and add
|
// device_construct_add_start(), but with a device_set_parent() call inserted between construct and add
|
||||||
bool construct_add_start(Device* device, Device* parent, const char* compatible);
|
bool construct_add_start(Device* device, Device* parent, const char* compatible);
|
||||||
|
|
||||||
|
// Same as construct_add_start(), but doesn't start the device - for a device registered with
|
||||||
|
// device_hotplug_register() instead, whose driver has a Driver::probe deciding if/when it starts.
|
||||||
|
bool construct_add(Device* device, Device* parent, const char* compatible);
|
||||||
|
|
||||||
// Unpacks a vendor-typed init-cmd array (ili9881c_lcd_init_cmd_t / st7123_lcd_init_cmd_t - same
|
// Unpacks a vendor-typed init-cmd array (ili9881c_lcd_init_cmd_t / st7123_lcd_init_cmd_t - same
|
||||||
// field layout) into the flattened [cmd, data_len, delay_ms, data_len bytes...] byte encoding that
|
// field layout) into the flattened [cmd, data_len, delay_ms, data_len bytes...] byte encoding that
|
||||||
// ili9881c-module/st7123-module's init-sequence config field expects (the same encoding a
|
// ili9881c-module/st7123-module's init-sequence config field expects (the same encoding a
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/queue.h>
|
#include <freertos/queue.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -262,9 +263,9 @@ static bool write_reg_fast(Device* device, uint8_t reg, uint8_t value) {
|
|||||||
return i2c_controller_write_register(parent, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(2)) == ERROR_NONE;
|
return i2c_controller_write_register(parent, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(2)) == ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tab5_keyboard_is_attached(Device* device) {
|
static error_t probe(Device* device) {
|
||||||
auto* parent = device_get_parent(device);
|
auto* parent = device_get_parent(device);
|
||||||
return i2c_controller_has_device_at_address(parent, I2C_ADDRESS, pdMS_TO_TICKS(5)) == ERROR_NONE;
|
return i2c_controller_has_device_at_address(parent, I2C_ADDRESS, pdMS_TO_TICKS(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -462,7 +463,7 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
|
|||||||
// mode and interrupt configuration are volatile and reset to power-on defaults when the keyboard
|
// mode and interrupt configuration are volatile and reset to power-on defaults when the keyboard
|
||||||
// is unplugged and reconnected.
|
// is unplugged and reconnected.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
void tab5_keyboard_reinit(Device* device) {
|
static void tab5_keyboard_reinit(Device* device) {
|
||||||
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
|
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
|
||||||
write_reg_fast(device, REG_KEYBOARD_MODE, 0x00); // Normal mode
|
write_reg_fast(device, REG_KEYBOARD_MODE, 0x00); // Normal mode
|
||||||
write_reg_fast(device, REG_EVENT_NUM, 0x00); // flush event queue
|
write_reg_fast(device, REG_EVENT_NUM, 0x00); // flush event queue
|
||||||
@ -476,7 +477,7 @@ void tab5_keyboard_reinit(Device* device) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void tab5_keyboard_reset_state(Device* device) {
|
static void tab5_keyboard_reset_state(Device* device) {
|
||||||
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
|
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
|
||||||
|
|
||||||
for (uint8_t idx = 0; idx < 70U; idx++) {
|
for (uint8_t idx = 0; idx < 70U; idx++) {
|
||||||
@ -594,28 +595,26 @@ static error_t start(Device* device) {
|
|||||||
// device_get_driver_data().
|
// device_get_driver_data().
|
||||||
device_set_driver_data(device, internal);
|
device_set_driver_data(device, internal);
|
||||||
|
|
||||||
// This device is constructed speculatively at boot so it can be hot-plug-detected later - if
|
// Driver::probe (tab5_keyboard_is_attached) already confirmed presence before driver_bind()
|
||||||
// the keyboard isn't physically attached yet, skip reinit here (tab5_keyboard_attach_detect.cpp
|
// called this.
|
||||||
// calls it again once attach is confirmed) rather than issuing register writes that are certain
|
tab5_keyboard_reinit(device);
|
||||||
// to fail: unlike tab5_keyboard_is_attached()'s plain probe, write_register() logs at error
|
|
||||||
// level on failure (see esp32_i2c_master.cpp), which would be misleading noise for what's just
|
|
||||||
// "not plugged in yet".
|
|
||||||
if (tab5_keyboard_is_attached(device)) {
|
|
||||||
tab5_keyboard_reinit(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How long to give LVGL's own indev timer to drain the release events tab5_keyboard_reset_state()
|
||||||
|
// pushes below before the queue carrying them is deleted - longer than LVGL's default indev poll
|
||||||
|
// period so a key held across an unplug doesn't read as stuck forever.
|
||||||
|
static constexpr uint32_t STOP_DRAIN_GRACE_MS = 50;
|
||||||
|
|
||||||
static error_t stop(Device* device) {
|
static error_t stop(Device* device) {
|
||||||
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
|
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
|
||||||
|
|
||||||
remove_irq_pin(internal);
|
remove_irq_pin(internal);
|
||||||
write_reg(device, REG_INT_CFG, 0x00); // disable all interrupts
|
write_reg(device, REG_INT_CFG, 0x00); // disable all interrupts
|
||||||
internal->sym_active = false;
|
|
||||||
internal->aa_sticky = false;
|
tab5_keyboard_reset_state(device);
|
||||||
internal->aa_held = false;
|
vTaskDelay(pdMS_TO_TICKS(STOP_DRAIN_GRACE_MS));
|
||||||
update_leds(device, internal); // turn LEDs off
|
|
||||||
|
|
||||||
vQueueDelete(internal->queue);
|
vQueueDelete(internal->queue);
|
||||||
free(internal);
|
free(internal);
|
||||||
@ -659,7 +658,6 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
|||||||
static const KeyboardApi tab5_keyboard_api = {
|
static const KeyboardApi tab5_keyboard_api = {
|
||||||
.read_key = tab5_keyboard_read_key,
|
.read_key = tab5_keyboard_read_key,
|
||||||
.get_backlight = nullptr,
|
.get_backlight = nullptr,
|
||||||
.is_present = tab5_keyboard_is_attached,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
|
// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
|
||||||
@ -671,6 +669,7 @@ Driver tab5_keyboard_driver = {
|
|||||||
.compatible = (const char*[]) { "m5stack,tab5-keyboard", nullptr },
|
.compatible = (const char*[]) { "m5stack,tab5-keyboard", nullptr },
|
||||||
.start_device = start,
|
.start_device = start,
|
||||||
.stop_device = stop,
|
.stop_device = stop,
|
||||||
|
.probe = probe,
|
||||||
.api = &tab5_keyboard_api,
|
.api = &tab5_keyboard_api,
|
||||||
.device_type = &KEYBOARD_TYPE,
|
.device_type = &KEYBOARD_TYPE,
|
||||||
.owner = &m5stack_tab5_module,
|
.owner = &m5stack_tab5_module,
|
||||||
@ -684,10 +683,10 @@ static Device tab5_keyboard_device {};
|
|||||||
|
|
||||||
// The keyboard accessory is a kernel driver device (m5stack,tab5-keyboard, defined directly in
|
// The keyboard accessory is a kernel driver device (m5stack,tab5-keyboard, defined directly in
|
||||||
// this project). Unlike the display/touch, it isn't gated on the display-variant detection at
|
// this project). Unlike the display/touch, it isn't gated on the display-variant detection at
|
||||||
// all (it lives on i2c2, a separate bus) - lvgl-module binds its indev unconditionally at boot
|
// all (it lives on i2c2, a separate bus). Registered with the kernel's hotplug poller instead of
|
||||||
// regardless of physical attach state. Hot-plug attach/detach handling (register reinit, LVGL
|
// started here - Driver::probe (tab5_keyboard_is_attached) decides when it starts/stops. LVGL
|
||||||
// rotation) lives in tab5_keyboard_attach_detect.cpp, not this driver - see module.cpp for where
|
// rotation-on-attach lives in tab5_keyboard_attach_detect.cpp, not this driver - see module.cpp
|
||||||
// that gets started.
|
// for where that gets started.
|
||||||
void tab5_create_keyboard(Device* i2c2) {
|
void tab5_create_keyboard(Device* i2c2) {
|
||||||
tab5_keyboard_device = Device {
|
tab5_keyboard_device = Device {
|
||||||
.address = 0,
|
.address = 0,
|
||||||
@ -715,7 +714,9 @@ void tab5_create_keyboard(Device* i2c2) {
|
|||||||
|
|
||||||
// Parented to i2c2 itself (not root, unlike the display): the keyboard driver's start() uses
|
// Parented to i2c2 itself (not root, unlike the display): the keyboard driver's start() uses
|
||||||
// device_get_parent() as its I2C bus controller.
|
// device_get_parent() as its I2C bus controller.
|
||||||
construct_add_start(&tab5_keyboard_device, i2c2, "m5stack,tab5-keyboard");
|
if (construct_add(&tab5_keyboard_device, i2c2, "m5stack,tab5-keyboard")) {
|
||||||
|
device_hotplug_register(&tab5_keyboard_device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|||||||
@ -11,38 +11,23 @@ extern "C" {
|
|||||||
#include <tactility/drivers/gpio.h>
|
#include <tactility/drivers/gpio.h>
|
||||||
|
|
||||||
struct Tab5KeyboardConfig {
|
struct Tab5KeyboardConfig {
|
||||||
// Fixed 0x6D on this keyboard accessory - kept as a field for parity/documentation with
|
/** Fixed 0x6D - not used to probe the bus, kept only for parity with other dynamic devices. */
|
||||||
// other dynamically-constructed devices in this project, not used to probe the bus.
|
|
||||||
uint8_t address;
|
uint8_t address;
|
||||||
// Native SoC GPIO (e.g. gpio0 pin 50) wired directly to the keyboard's INT line - not routed
|
/**
|
||||||
// through an IO expander, since it needs a real hardware ISR for responsive key events.
|
* Wired directly to a native SoC GPIO (not an IO expander) for a real ISR.
|
||||||
// GPIO_PIN_SPEC_NONE falls back to polling REG_INT_STAT instead.
|
* GPIO_PIN_SPEC_NONE falls back to polling REG_INT_STAT.
|
||||||
|
*/
|
||||||
struct GpioPinSpec pin_interrupt;
|
struct GpioPinSpec pin_interrupt;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern struct Driver tab5_keyboard_driver;
|
extern struct Driver tab5_keyboard_driver;
|
||||||
|
|
||||||
// Constructs and starts the keyboard accessory device on i2c2. Called from display_detect.cpp's
|
/**
|
||||||
// on_display_detect_event() once i2c2 is up.
|
* @brief Constructs (but doesn't start) the keyboard device on i2c2 and registers it with the
|
||||||
|
* kernel's hotplug poller. Called once i2c2 is up (see display_detect.cpp).
|
||||||
|
*/
|
||||||
void tab5_create_keyboard(struct Device* i2c2);
|
void tab5_create_keyboard(struct Device* i2c2);
|
||||||
|
|
||||||
// Returns true if the keyboard accessory currently ACKs on the I2C bus. Cheap bus probe, no
|
|
||||||
// debouncing - callers wanting hot-plug-stable state (e.g. tab5_keyboard_attach_detect.cpp)
|
|
||||||
// should debounce across their own polling interval.
|
|
||||||
bool tab5_keyboard_is_attached(struct Device* device);
|
|
||||||
|
|
||||||
// (Re)applies the device's register configuration - RGB mode, brightness, interrupt config, LED
|
|
||||||
// state. Volatile on this chip: reset to power-on defaults whenever the keyboard is unplugged and
|
|
||||||
// reconnected, so callers must call this again after confirming a reattach (see
|
|
||||||
// tab5_keyboard_attach_detect.cpp).
|
|
||||||
void tab5_keyboard_reinit(struct Device* device);
|
|
||||||
|
|
||||||
// Emits a release for every currently-held key (a hardware release can't arrive once the keyboard
|
|
||||||
// is unplugged), then clears held-key/software-repeat/modifier state. Callers must call this on
|
|
||||||
// confirmed detach (see tab5_keyboard_attach_detect.cpp) so a key held across an unplug doesn't
|
|
||||||
// leave consumers with a stuck key or spurious repeats/modifiers after reattach.
|
|
||||||
void tab5_keyboard_reset_state(struct Device* device);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -1,173 +0,0 @@
|
|||||||
#include "tab5_keyboard_attach_detect.h"
|
|
||||||
|
|
||||||
#include "tab5_keyboard.h"
|
|
||||||
|
|
||||||
#include <tactility/device.h>
|
|
||||||
#include <tactility/log.h>
|
|
||||||
|
|
||||||
#include <lvgl/lvgl.h>
|
|
||||||
#include <lvgl.h>
|
|
||||||
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/timers.h>
|
|
||||||
|
|
||||||
constexpr auto* TAG = "Tab5";
|
|
||||||
|
|
||||||
// Hot-plug attach-state check interval. I2C probes can false-positive on a floating/half-connected
|
|
||||||
// bus (e.g. mid-unplug), so a state change is only acted on once it's seen on two consecutive
|
|
||||||
// checks in a row.
|
|
||||||
constexpr auto ATTACH_CHECK_INTERVAL_MS = 1000;
|
|
||||||
|
|
||||||
static TimerHandle_t attach_detect_timer = nullptr;
|
|
||||||
|
|
||||||
static bool was_attached = false;
|
|
||||||
static bool pending_attach_state = false;
|
|
||||||
static uint8_t pending_attach_confirm_count = 0;
|
|
||||||
|
|
||||||
// Tracks LVGL's own readiness so a restart (lvgl_is_running() going from false back to true -
|
|
||||||
// e.g. an app that took over the display for direct rendering, stopping and letting LVGL rebind)
|
|
||||||
// can be told apart from the keyboard itself attaching/detaching. See apply_state()'s comment for
|
|
||||||
// why that distinction matters.
|
|
||||||
static bool was_lvgl_ready = false;
|
|
||||||
|
|
||||||
static lv_display_rotation_t saved_rotation = LV_DISPLAY_ROTATION_0;
|
|
||||||
static bool rotation_override_active = false;
|
|
||||||
|
|
||||||
// Applies the current attach state to LVGL (landscape rotation while attached, restoring the
|
|
||||||
// prior rotation on detach unless the user changed it manually since attaching) and to the
|
|
||||||
// keyboard device's own register state (reinit on attach - RGB mode/interrupt config are volatile
|
|
||||||
// across an unplug/replug on this chip). Ported as-is from the deprecated HAL's
|
|
||||||
// Tab5Keyboard::applyAutoRotation() / the pre-refactor tab5_keyboard.cpp driver logic.
|
|
||||||
// @return true once handled; false to be retried on the next tick (e.g. LVGL lock busy).
|
|
||||||
static bool apply_state(Device* keyboard_device, bool attached) {
|
|
||||||
if (!lvgl_try_lock(pdMS_TO_TICKS(100))) {
|
|
||||||
return false; // retry next tick
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolved inside the lock, not before: the default display can start/stop between an
|
|
||||||
// unlocked probe and actually taking the lock, and lv_display_get_default() itself isn't
|
|
||||||
// safe to call without holding it (unlike lvgl_is_running(), used for the readiness check in
|
|
||||||
// attach_detect_callback()).
|
|
||||||
auto* display = lv_display_get_default();
|
|
||||||
if (display == nullptr) {
|
|
||||||
lvgl_unlock();
|
|
||||||
return false; // LVGL not ready yet - retry next tick
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attached) {
|
|
||||||
tab5_keyboard_reinit(keyboard_device);
|
|
||||||
|
|
||||||
if (lv_display_get_rotation(display) != LV_DISPLAY_ROTATION_90) {
|
|
||||||
saved_rotation = lv_display_get_rotation(display);
|
|
||||||
rotation_override_active = true;
|
|
||||||
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// A key held at the moment of unplug can never get its hardware release - reset tracked
|
|
||||||
// state so it doesn't read as stuck or feed stale modifiers into whatever's pressed next
|
|
||||||
// after reattach.
|
|
||||||
tab5_keyboard_reset_state(keyboard_device);
|
|
||||||
|
|
||||||
// Only restore if rotation is still what we set it to - if the user manually changed it
|
|
||||||
// since attaching, respect their choice instead.
|
|
||||||
if (rotation_override_active && lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
|
|
||||||
lv_display_set_rotation(display, saved_rotation);
|
|
||||||
}
|
|
||||||
rotation_override_active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lvgl_unlock();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void attach_detect_callback(TimerHandle_t /*timer*/) {
|
|
||||||
Device* keyboard_device = nullptr;
|
|
||||||
if (device_get_by_name("keyboard0", &keyboard_device) != ERROR_NONE) {
|
|
||||||
return; // Not constructed yet - will retry on next tick
|
|
||||||
}
|
|
||||||
|
|
||||||
// LVGL restarting is a distinct event from the keyboard physically attaching/detaching: the
|
|
||||||
// accessory may never have moved, but whatever apply_state() last set (rotation) may have
|
|
||||||
// been reset in the meantime by the restart. Forcing was_attached false makes the block below
|
|
||||||
// see a fresh "attached" transition, still going through the normal two-check debounce, so
|
|
||||||
// apply_state() re-announces the current state instead of staying silent forever, waiting for
|
|
||||||
// an edge that will never come because the keyboard was never actually unplugged.
|
|
||||||
// lvgl_is_running() is safe to call unlocked (unlike lv_display_get_default(), resolved inside
|
|
||||||
// the lock in apply_state() instead).
|
|
||||||
const bool lvgl_ready = lvgl_is_running();
|
|
||||||
if (lvgl_ready && !was_lvgl_ready) {
|
|
||||||
// If the keyboard is (and, per was_attached, already was) physically detached, forcing
|
|
||||||
// was_attached false below means the detach transition below will never fire again for
|
|
||||||
// this unplug - it already happened before this restart. Reset software state here
|
|
||||||
// instead, since apply_state()'s own detach path may never have run: it could have bailed
|
|
||||||
// out early (LVGL lock busy, or display not ready yet) before reaching its
|
|
||||||
// tab5_keyboard_reset_state() call, and now never will, because that transition is about
|
|
||||||
// to be erased. This reset doesn't touch LVGL/display state, so it doesn't need
|
|
||||||
// apply_state()'s lock/display gating.
|
|
||||||
if (was_attached && !tab5_keyboard_is_attached(keyboard_device)) {
|
|
||||||
tab5_keyboard_reset_state(keyboard_device);
|
|
||||||
}
|
|
||||||
was_attached = false;
|
|
||||||
pending_attach_confirm_count = 0;
|
|
||||||
}
|
|
||||||
was_lvgl_ready = lvgl_ready;
|
|
||||||
|
|
||||||
const bool attached = tab5_keyboard_is_attached(keyboard_device);
|
|
||||||
if (attached != was_attached) {
|
|
||||||
// Require the new state to be confirmed on a second consecutive check before acting: a
|
|
||||||
// single probe on a floating/half-connected bus (e.g. mid-unplug) can false-positive.
|
|
||||||
if (attached != pending_attach_state || pending_attach_confirm_count == 0) {
|
|
||||||
pending_attach_state = attached;
|
|
||||||
pending_attach_confirm_count = 1;
|
|
||||||
} else {
|
|
||||||
pending_attach_confirm_count = 0;
|
|
||||||
if (apply_state(keyboard_device, attached)) {
|
|
||||||
was_attached = attached;
|
|
||||||
}
|
|
||||||
// else: not handled yet (e.g. LVGL lock busy); retry on the next confirmed check
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pending_attach_confirm_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
device_put(keyboard_device);
|
|
||||||
}
|
|
||||||
|
|
||||||
void tab5_keyboard_attach_detect_start() {
|
|
||||||
if (attach_detect_timer != nullptr) {
|
|
||||||
LOG_W(TAG, "keyboard attach-detect timer already running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
was_attached = false;
|
|
||||||
pending_attach_confirm_count = 0;
|
|
||||||
was_lvgl_ready = false;
|
|
||||||
rotation_override_active = false;
|
|
||||||
|
|
||||||
attach_detect_timer = xTimerCreate("kb_attach_detect", pdMS_TO_TICKS(ATTACH_CHECK_INTERVAL_MS), pdTRUE, nullptr, attach_detect_callback);
|
|
||||||
if (!attach_detect_timer) {
|
|
||||||
LOG_E(TAG, "Failed to create keyboard attach-detect timer");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (xTimerStart(attach_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
|
|
||||||
LOG_E(TAG, "Failed to start keyboard attach-detect timer");
|
|
||||||
xTimerDelete(attach_detect_timer, pdMS_TO_TICKS(100));
|
|
||||||
attach_detect_timer = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void tab5_keyboard_attach_detect_stop() {
|
|
||||||
if (attach_detect_timer == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xTimerStop(attach_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
|
|
||||||
LOG_W(TAG, "Failed to stop keyboard attach-detect timer");
|
|
||||||
}
|
|
||||||
if (xTimerDelete(attach_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
|
|
||||||
LOG_E(TAG, "Failed to delete keyboard attach-detect timer");
|
|
||||||
}
|
|
||||||
// Always clear the handle - stale non-null handle is worse than a resource leak, as it would
|
|
||||||
// cause tab5_keyboard_attach_detect_start() to silently skip re-creating the timer.
|
|
||||||
attach_detect_timer = nullptr;
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
// Starts/stops the periodic keyboard-accessory hot-plug poll: reapplies register configuration on
|
|
||||||
// reattach (see tab5_keyboard_reinit()) and switches LVGL to landscape while attached, restoring
|
|
||||||
// the prior rotation on detach - unless the user changed it manually since attaching, in which
|
|
||||||
// case their choice is respected. Also re-announces the current attach state after LVGL itself
|
|
||||||
// restarts (e.g. an app that took over the display for direct rendering, stopping and letting
|
|
||||||
// LVGL rebind), since that's a distinct event from the keyboard physically attaching/detaching -
|
|
||||||
// the accessory may never have moved, but the rotation override it applied may have been reset in
|
|
||||||
// the meantime. Called from module.cpp's start()/stop().
|
|
||||||
void tab5_keyboard_attach_detect_start();
|
|
||||||
void tab5_keyboard_attach_detect_stop();
|
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
#include "tab5_keyboard_lvgl_rotation.h"
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/device_listener.h>
|
||||||
|
|
||||||
|
#include <lvgl/lvgl.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
static lv_display_rotation_t saved_rotation = LV_DISPLAY_ROTATION_0;
|
||||||
|
static bool rotation_override_active = false;
|
||||||
|
|
||||||
|
static void apply_rotation(bool attached) {
|
||||||
|
lvgl_lock();
|
||||||
|
|
||||||
|
auto* display = lv_display_get_default();
|
||||||
|
if (display == nullptr) {
|
||||||
|
lvgl_unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attached) {
|
||||||
|
if (lv_display_get_rotation(display) != LV_DISPLAY_ROTATION_90) {
|
||||||
|
saved_rotation = lv_display_get_rotation(display);
|
||||||
|
rotation_override_active = true;
|
||||||
|
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
|
||||||
|
}
|
||||||
|
} else if (rotation_override_active) {
|
||||||
|
// Ownership of the rotation ends here regardless - only restore it if the display is
|
||||||
|
// still at what we set it to (the user may have changed it manually while attached).
|
||||||
|
if (lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
|
||||||
|
lv_display_set_rotation(display, saved_rotation);
|
||||||
|
}
|
||||||
|
rotation_override_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_device_event(Device* device, DeviceEvent event, void*) {
|
||||||
|
if (!device_is_compatible(device, "m5stack,tab5-keyboard")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event == DEVICE_EVENT_STARTED) {
|
||||||
|
apply_rotation(true);
|
||||||
|
} else if (event == DEVICE_EVENT_STOPPED) {
|
||||||
|
apply_rotation(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void tab5_keyboard_lvgl_rotation_start() {
|
||||||
|
rotation_override_active = false;
|
||||||
|
device_listener_add(on_device_event, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tab5_keyboard_lvgl_rotation_stop() {
|
||||||
|
apply_rotation(false);
|
||||||
|
device_listener_remove(on_device_event);
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Applies landscape rotation while the keyboard accessory ("keyboard0") is started, restoring the
|
||||||
|
// prior rotation on stop - unless the user changed it manually since attaching, in which case
|
||||||
|
// their choice is respected. Start/stop are now driven by the kernel's hotplug poller (see
|
||||||
|
// tab5_keyboard.cpp's Driver::probe), not polled here. Called from module.cpp's start()/stop().
|
||||||
|
void tab5_keyboard_lvgl_rotation_start();
|
||||||
|
void tab5_keyboard_lvgl_rotation_stop();
|
||||||
@ -13,7 +13,7 @@
|
|||||||
#include "devices/detect.h"
|
#include "devices/detect.h"
|
||||||
#include "devices/tab5_headphone_detect.h"
|
#include "devices/tab5_headphone_detect.h"
|
||||||
#include "devices/tab5_keyboard.h"
|
#include "devices/tab5_keyboard.h"
|
||||||
#include "devices/tab5_keyboard_attach_detect.h"
|
#include "devices/tab5_keyboard_lvgl_rotation.h"
|
||||||
#include "devices/tab5_power_control.h"
|
#include "devices/tab5_power_control.h"
|
||||||
#include "devices/tab_5_camera.h"
|
#include "devices/tab_5_camera.h"
|
||||||
|
|
||||||
@ -70,12 +70,12 @@ static error_t start() {
|
|||||||
tab5_camera_init();
|
tab5_camera_init();
|
||||||
device_listener_add(on_io_expander0_started, nullptr);
|
device_listener_add(on_io_expander0_started, nullptr);
|
||||||
tab5_headphone_detect_start();
|
tab5_headphone_detect_start();
|
||||||
tab5_keyboard_attach_detect_start();
|
tab5_keyboard_lvgl_rotation_start();
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static error_t stop() {
|
static error_t stop() {
|
||||||
tab5_keyboard_attach_detect_stop();
|
tab5_keyboard_lvgl_rotation_stop();
|
||||||
tab5_headphone_detect_stop();
|
tab5_headphone_detect_stop();
|
||||||
device_listener_remove(on_io_expander0_started);
|
device_listener_remove(on_io_expander0_started);
|
||||||
tab5_detect_stop();
|
tab5_detect_stop();
|
||||||
|
|||||||
@ -10,13 +10,13 @@ constexpr auto* TAG = "FreeRTOS";
|
|||||||
|
|
||||||
namespace simulator {
|
namespace simulator {
|
||||||
|
|
||||||
MainFunction mainFunction = nullptr;
|
static MainFunction mainFunction = nullptr;
|
||||||
|
|
||||||
void setMain(MainFunction newMainFunction) {
|
void setMain(MainFunction newMainFunction) {
|
||||||
mainFunction = newMainFunction;
|
mainFunction = newMainFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void freertosMainTask(void* parameter) {
|
static void freertosMainTask(void*) {
|
||||||
LOG_I(TAG, "starting app_main()");
|
LOG_I(TAG, "starting app_main()");
|
||||||
assert(simulator::mainFunction);
|
assert(simulator::mainFunction);
|
||||||
mainFunction();
|
mainFunction();
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Main.h"
|
#include "Main.h"
|
||||||
|
#include "drivers/sdl_bridge.h"
|
||||||
|
|
||||||
|
#include <csignal>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
namespace simulator {
|
namespace simulator {
|
||||||
/** Set the function pointer of the real app_main() */
|
/** Set the function pointer of the real app_main() */
|
||||||
@ -14,8 +19,23 @@ void app_main(); // ESP-IDF's main function, implemented in the application
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
// Actual main function that passes on app_main() (to be executed in a FreeRTOS task) and bootstraps FreeRTOS
|
// The FreeRTOS POSIX port arms a process-wide SIGALRM timer for its tick and expects every one
|
||||||
|
// of its task pthreads to have all signals but SIGINT blocked.
|
||||||
|
// (see prvSetupSignalsAndSchedulerPolicy() in FreeRTOS-Kernel's Posix port.c)
|
||||||
|
// A signal-generated SIGALRM can land on any thread in the process that doesn't block it.
|
||||||
|
// This thread stays a plain OS thread (running the SDL loop below, never a FreeRTOS task),
|
||||||
|
// so without this it's eligible to catch a tick SIGALRM and freeze inside the scheduler's handler.
|
||||||
|
// Block the same set here, before anything else, so it never can.
|
||||||
|
sigset_t all_signals_except_sigint;
|
||||||
|
sigfillset(&all_signals_except_sigint);
|
||||||
|
sigdelset(&all_signals_except_sigint, SIGINT);
|
||||||
|
pthread_sigmask(SIG_SETMASK, &all_signals_except_sigint, nullptr);
|
||||||
|
|
||||||
|
// FreeRTOS and app_main() run on a separate thread: macOS requires SDL/Cocoa window creation,
|
||||||
|
// event pumping and rendering to happen on the real OS main thread, which sdl_bridge_run_main_loop()
|
||||||
|
// below takes over. freertosMain() never returns, so this thread is detached rather than joined.
|
||||||
simulator::setMain(app_main);
|
simulator::setMain(app_main);
|
||||||
simulator::freertosMain();
|
std::thread(simulator::freertosMain).detach();
|
||||||
|
sdl_bridge_run_main_loop();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
64
Devices/simulator/Source/drivers/sdl_bridge.cpp
Normal file
64
Devices/simulator/Source/drivers/sdl_bridge.cpp
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include "sdl_bridge.h"
|
||||||
|
#include "sdl_input.h"
|
||||||
|
|
||||||
|
#include <tactility/error.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct PresentJob {
|
||||||
|
Device* device;
|
||||||
|
void* internal;
|
||||||
|
int32_t x_start;
|
||||||
|
int32_t y_start;
|
||||||
|
int32_t x_end;
|
||||||
|
int32_t y_end;
|
||||||
|
const void* color_data;
|
||||||
|
error_t result;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::mutex job_mutex;
|
||||||
|
std::condition_variable job_ready_cv;
|
||||||
|
std::condition_variable job_done_cv;
|
||||||
|
bool job_pending = false;
|
||||||
|
bool job_done = false;
|
||||||
|
PresentJob pending_job;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t sdl_bridge_present(Device* device, void* internal, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||||
|
std::unique_lock<std::mutex> lock(job_mutex);
|
||||||
|
|
||||||
|
pending_job = { device, internal, x_start, y_start, x_end, y_end, color_data, ERROR_NONE };
|
||||||
|
job_pending = true;
|
||||||
|
job_done = false;
|
||||||
|
job_ready_cv.notify_one();
|
||||||
|
|
||||||
|
job_done_cv.wait(lock, [] { return job_done; });
|
||||||
|
return pending_job.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sdl_bridge_run_main_loop() {
|
||||||
|
while (true) {
|
||||||
|
sdl_input_pump();
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(job_mutex);
|
||||||
|
if (job_ready_cv.wait_for(lock, std::chrono::milliseconds(1), [] { return job_pending; })) {
|
||||||
|
PresentJob job = pending_job;
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
job.result = sdl_display_execute_draw_bitmap(job.device, job.internal, job.x_start, job.y_start, job.x_end, job.y_end, job.color_data);
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
pending_job.result = job.result;
|
||||||
|
job_pending = false;
|
||||||
|
job_done = true;
|
||||||
|
lock.unlock();
|
||||||
|
job_done_cv.notify_one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Devices/simulator/Source/drivers/sdl_bridge.h
Normal file
40
Devices/simulator/Source/drivers/sdl_bridge.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <tactility/error.h>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct Device;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Runs forever, pumping SDL input and executing display present jobs submitted via
|
||||||
|
* sdl_bridge_present(). Must be called exactly once, from the real OS main thread: macOS requires
|
||||||
|
* SDL/Cocoa window creation, event pumping and rendering to happen there, but FreeRTOS tasks
|
||||||
|
* (including the lvgl task that owns display flush and indev polling) run on separate pthreads
|
||||||
|
* spawned by the FreeRTOS POSIX port, not on that thread.
|
||||||
|
*/
|
||||||
|
void sdl_bridge_run_main_loop(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Hands a display flush off to the main thread and blocks until it has finished copying
|
||||||
|
* the pixel data out (see sdl_display_execute_draw_bitmap() in sdl_display.cpp). Called from the
|
||||||
|
* lvgl task. Must block: the caller's pixel buffer is single-buffered and gets reused as soon as
|
||||||
|
* this returns.
|
||||||
|
*/
|
||||||
|
error_t sdl_bridge_present(struct Device* device, void* internal, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Implemented in sdl_display.cpp: the actual SDL work behind a display flush (lazy window
|
||||||
|
* init on first call, SDL_UpdateTexture, present). Only ever called from sdl_bridge_run_main_loop()
|
||||||
|
* on the main thread.
|
||||||
|
*/
|
||||||
|
error_t sdl_display_execute_draw_bitmap(struct Device* device, void* internal, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
#include "sdl_display.h"
|
#include "sdl_display.h"
|
||||||
|
#include "sdl_bridge.h"
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/driver.h>
|
#include <tactility/driver.h>
|
||||||
@ -141,8 +142,10 @@ static bool sdl_display_lazy_init(Device* device, SdlDisplayInternal* internal)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
// Only ever called from sdl_bridge_run_main_loop() on the real main thread - required for
|
||||||
auto* internal = static_cast<SdlDisplayInternal*>(device_get_driver_data(device));
|
// SDL/Cocoa window creation and rendering on macOS.
|
||||||
|
error_t sdl_display_execute_draw_bitmap(Device* device, void* internal_ptr, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||||
|
auto* internal = static_cast<SdlDisplayInternal*>(internal_ptr);
|
||||||
|
|
||||||
if (internal->init_failed) {
|
if (internal->init_failed) {
|
||||||
return ERROR_RESOURCE;
|
return ERROR_RESOURCE;
|
||||||
@ -166,6 +169,16 @@ static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t
|
|||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||||
|
auto* internal = static_cast<SdlDisplayInternal*>(device_get_driver_data(device));
|
||||||
|
|
||||||
|
if (internal->init_failed) {
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdl_bridge_present(device, internal, x_start, y_start, x_end, y_end, color_data);
|
||||||
|
}
|
||||||
|
|
||||||
static enum DisplayColorFormat sdl_display_get_color_format(Device*) {
|
static enum DisplayColorFormat sdl_display_get_color_format(Device*) {
|
||||||
return DISPLAY_COLOR_FORMAT_RGB565;
|
return DISPLAY_COLOR_FORMAT_RGB565;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,16 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr size_t KEY_QUEUE_CAPACITY = 32;
|
constexpr size_t KEY_QUEUE_CAPACITY = 32;
|
||||||
|
|
||||||
|
// Written by sdl_input_pump() on the real main thread, read by sdl_input_get_pointer_state()/
|
||||||
|
// sdl_input_pop_key()/sdl_input_has_queued_key() on the lvgl task.
|
||||||
|
std::mutex state_mutex;
|
||||||
|
|
||||||
SdlPointerState pointer_state = { 0, 0, false };
|
SdlPointerState pointer_state = { 0, 0, false };
|
||||||
|
|
||||||
uint32_t key_queue[KEY_QUEUE_CAPACITY];
|
uint32_t key_queue[KEY_QUEUE_CAPACITY];
|
||||||
@ -73,62 +78,78 @@ uint32_t keycode_to_key(SDL_Keycode sdl_key, bool shift) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sdl_input_pump() {
|
void sdl_input_pump() {
|
||||||
if (!text_input_started) {
|
// exit() must run with state_mutex unlocked: it never returns, so a lock_guard held across it
|
||||||
SDL_StartTextInput();
|
// would never release the mutex, hanging any other thread that later calls into this file's
|
||||||
text_input_started = true;
|
// other functions (all of which lock state_mutex) while exit() tears the process down.
|
||||||
|
bool quit_requested = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
|
||||||
|
if (!text_input_started) {
|
||||||
|
SDL_StartTextInput();
|
||||||
|
text_input_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
set_pointer_position(event.motion.x, event.motion.y);
|
||||||
|
break;
|
||||||
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
|
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
// event.button.x/y can be stale immediately after a window resize (an
|
||||||
|
// SDL/X11 event-queue quirk - confirmed by comparing against a live
|
||||||
|
// SDL_GetWindowSize() at the same instant). SDL_GetMouseState() queries the
|
||||||
|
// OS for the current pointer position directly, sidestepping that entirely.
|
||||||
|
int live_x, live_y;
|
||||||
|
SDL_GetMouseState(&live_x, &live_y);
|
||||||
|
set_pointer_position(live_x, live_y);
|
||||||
|
pointer_state.pressed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_MOUSEBUTTONUP:
|
||||||
|
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
pointer_state.pressed = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_KEYDOWN:
|
||||||
|
push_key(keycode_to_key(event.key.keysym.sym, (event.key.keysym.mod & KMOD_SHIFT) != 0));
|
||||||
|
break;
|
||||||
|
case SDL_TEXTINPUT:
|
||||||
|
// ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard.
|
||||||
|
push_key(static_cast<uint8_t>(event.text.text[0]));
|
||||||
|
break;
|
||||||
|
case SDL_WINDOWEVENT:
|
||||||
|
// Resizing doesn't change what LVGL last rendered, only how large it should
|
||||||
|
// appear - re-present the existing frame at the new scale immediately, rather
|
||||||
|
// than leaving stale-looking content on screen until the next LVGL-driven flush.
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
|
sdl_display_present_now();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_QUIT:
|
||||||
|
quit_requested = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Event event;
|
if (quit_requested) {
|
||||||
while (SDL_PollEvent(&event)) {
|
exit(0);
|
||||||
switch (event.type) {
|
|
||||||
case SDL_MOUSEMOTION:
|
|
||||||
set_pointer_position(event.motion.x, event.motion.y);
|
|
||||||
break;
|
|
||||||
case SDL_MOUSEBUTTONDOWN:
|
|
||||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
|
||||||
// event.button.x/y can be stale immediately after a window resize (an
|
|
||||||
// SDL/X11 event-queue quirk - confirmed by comparing against a live
|
|
||||||
// SDL_GetWindowSize() at the same instant). SDL_GetMouseState() queries the
|
|
||||||
// OS for the current pointer position directly, sidestepping that entirely.
|
|
||||||
int live_x, live_y;
|
|
||||||
SDL_GetMouseState(&live_x, &live_y);
|
|
||||||
set_pointer_position(live_x, live_y);
|
|
||||||
pointer_state.pressed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SDL_MOUSEBUTTONUP:
|
|
||||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
|
||||||
pointer_state.pressed = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SDL_KEYDOWN:
|
|
||||||
push_key(keycode_to_key(event.key.keysym.sym, (event.key.keysym.mod & KMOD_SHIFT) != 0));
|
|
||||||
break;
|
|
||||||
case SDL_TEXTINPUT:
|
|
||||||
// ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard.
|
|
||||||
push_key(static_cast<uint8_t>(event.text.text[0]));
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT:
|
|
||||||
// Resizing doesn't change what LVGL last rendered, only how large it should
|
|
||||||
// appear - re-present the existing frame at the new scale immediately, rather
|
|
||||||
// than leaving stale-looking content on screen until the next LVGL-driven flush.
|
|
||||||
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
|
||||||
sdl_display_present_now();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SDL_QUIT:
|
|
||||||
exit(0);
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sdl_input_get_pointer_state(SdlPointerState* out_state) {
|
void sdl_input_get_pointer_state(SdlPointerState* out_state) {
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
*out_state = pointer_state;
|
*out_state = pointer_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sdl_input_pop_key(uint32_t* out_key) {
|
bool sdl_input_pop_key(uint32_t* out_key) {
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
if (key_queue_count == 0) {
|
if (key_queue_count == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -139,5 +160,6 @@ bool sdl_input_pop_key(uint32_t* out_key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool sdl_input_has_queued_key() {
|
bool sdl_input_has_queued_key() {
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
return key_queue_count > 0;
|
return key_queue_count > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,9 @@ struct SdlPointerState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Drains all pending SDL events exactly once, updating the pointer state and key queue
|
* @brief Drains all pending SDL events exactly once, updating the pointer state and key queue
|
||||||
* below. Safe to call from both the sdl-pointer and sdl-keyboard drivers' polling functions:
|
* below. Must be called only from the real OS main thread (sdl_bridge_run_main_loop()): SDL
|
||||||
* SDL_PollEvent() drains a single global queue, so whichever driver is polled first on a given
|
* requires event pumping to happen there on macOS. The getters below are safe to call from a
|
||||||
* LVGL indev tick pumps events for both.
|
* different thread (the lvgl task, via sdl-pointer/sdl-keyboard's polling functions).
|
||||||
*/
|
*/
|
||||||
void sdl_input_pump(void);
|
void sdl_input_pump(void);
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,6 @@ static error_t stop(Device*) { return ERROR_NONE; }
|
|||||||
// region KeyboardApi
|
// region KeyboardApi
|
||||||
|
|
||||||
static error_t sdl_keyboard_read_key(Device*, KeyboardKeyData* data) {
|
static error_t sdl_keyboard_read_key(Device*, KeyboardKeyData* data) {
|
||||||
sdl_input_pump();
|
|
||||||
|
|
||||||
uint32_t key = 0;
|
uint32_t key = 0;
|
||||||
if (sdl_input_pop_key(&key)) {
|
if (sdl_input_pop_key(&key)) {
|
||||||
data->key = key;
|
data->key = key;
|
||||||
|
|||||||
@ -16,7 +16,6 @@ static error_t stop(Device*) { return ERROR_NONE; }
|
|||||||
// region PointerApi
|
// region PointerApi
|
||||||
|
|
||||||
static error_t sdl_pointer_read_data(Device*, TickType_t) {
|
static error_t sdl_pointer_read_data(Device*, TickType_t) {
|
||||||
sdl_input_pump();
|
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,15 +10,29 @@ tactility_add_module(app-module
|
|||||||
INCLUDE_DIRS include/
|
INCLUDE_DIRS include/
|
||||||
REQUIRES TactilityKernel service-module minitar
|
REQUIRES TactilityKernel service-module minitar
|
||||||
PRIV_REQUIRES TactilityKernelCpp
|
PRIV_REQUIRES TactilityKernelCpp
|
||||||
|
WHOLE_ARCHIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
|
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() rather
|
||||||
# rather than calling ::read/::write/::close() directly, on every platform whose build wraps
|
# than calling ::read/::write/::close() directly, on every platform where those are wrapped: via
|
||||||
# those symbols (ESP-IDF always; POSIX except macOS, whose linker doesn't support --wrap) -
|
# -Wl,--wrap= on ESP-IDF (see the top-level CMakeLists.txt) and non-Apple POSIX (below), or via
|
||||||
# see Tactility/CMakeLists.txt and the top-level CMakeLists.txt for where that's applied.
|
# source/stdio_wrap.cpp's dyld interpose on Apple (whose linker lacks --wrap).
|
||||||
if (NOT APPLE)
|
tactility_get_module_name(app-module MODULE_NAME)
|
||||||
|
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
|
||||||
|
|
||||||
|
# Routes every read()/write()/close() and printf-family call to source/stdio_wrap.cpp's __wrap_*
|
||||||
|
# functions; see that file for what they do and why. ESP-IDF wraps the same symbols itself (see
|
||||||
|
# the top-level CMakeLists.txt); Apple gets the same routing via dyld interpose instead, self-
|
||||||
|
# registered in that file. PUBLIC so every consumer of this module (Tactility, and any test target
|
||||||
|
# that links app-module directly) gets the flags too.
|
||||||
|
if (NOT ESP_PLATFORM AND NOT APPLE)
|
||||||
tactility_get_module_name(app-module MODULE_NAME)
|
tactility_get_module_name(app-module MODULE_NAME)
|
||||||
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
|
target_link_options(${MODULE_NAME} PUBLIC
|
||||||
|
"-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
|
||||||
|
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
||||||
|
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
||||||
|
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
||||||
|
)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it
|
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it
|
||||||
|
|||||||
@ -8,13 +8,42 @@
|
|||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
|
|
||||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
#if defined(TT_APP_IO_WRAPS_STDIO)
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
// Newlib's stdio (fflush()'s buffer-flush path, in particular) calls the reentrant _read_r/
|
||||||
|
// _write_r/_close_r syscall stubs directly. The plain read()/write()/close() newlib provides are
|
||||||
|
// just thin wrappers around them (esp-idf's components/newlib/src/syscalls.c: `write(fd, dst,
|
||||||
|
// size) { return _write_r(__getreent(), fd, dst, size); }`). Wrapping the _r stubs catches both;
|
||||||
|
// wrapping the plain names would only catch direct write()-style callers.
|
||||||
|
#include <reent.h>
|
||||||
|
extern "C" {
|
||||||
|
ssize_t __real__read_r(struct _reent* r, int fd, void* buffer, size_t size);
|
||||||
|
ssize_t __real__write_r(struct _reent* r, int fd, const void* buffer, size_t size);
|
||||||
|
int __real__close_r(struct _reent* r, int fd);
|
||||||
|
}
|
||||||
|
namespace {
|
||||||
|
ssize_t real_read(int fd, void* buffer, size_t size) { return __real__read_r(__getreent(), fd, buffer, size); }
|
||||||
|
ssize_t real_write(int fd, const void* buffer, size_t size) { return __real__write_r(__getreent(), fd, buffer, size); }
|
||||||
|
int real_close(int fd) { return __real__close_r(__getreent(), fd); }
|
||||||
|
} // namespace
|
||||||
|
#else
|
||||||
extern "C" {
|
extern "C" {
|
||||||
ssize_t __real_read(int fd, void* buffer, size_t size);
|
ssize_t __real_read(int fd, void* buffer, size_t size);
|
||||||
ssize_t __real_write(int fd, const void* buffer, size_t size);
|
ssize_t __real_write(int fd, const void* buffer, size_t size);
|
||||||
int __real_close(int fd);
|
int __real_close(int fd);
|
||||||
}
|
}
|
||||||
|
namespace {
|
||||||
|
ssize_t real_read(int fd, void* buffer, size_t size) { return __real_read(fd, buffer, size); }
|
||||||
|
ssize_t real_write(int fd, const void* buffer, size_t size) { return __real_write(fd, buffer, size); }
|
||||||
|
int real_close(int fd) { return __real_close(fd); }
|
||||||
|
} // namespace
|
||||||
|
#endif
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
namespace {
|
||||||
|
ssize_t real_read(int fd, void* buffer, size_t size) { return ::read(fd, buffer, size); }
|
||||||
|
ssize_t real_write(int fd, const void* buffer, size_t size) { return ::write(fd, buffer, size); }
|
||||||
|
int real_close(int fd) { return ::close(fd); }
|
||||||
|
} // namespace
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -56,11 +85,7 @@ ssize_t app_io_read(int fd, void* buffer, size_t size) {
|
|||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
return real_read(fd, buffer, size);
|
||||||
return __real_read(fd, buffer, size);
|
|
||||||
#else
|
|
||||||
return ::read(fd, buffer, size);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
||||||
@ -78,22 +103,14 @@ ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
|||||||
// output is currently being intercepted. Without this, a log line emitted while any app
|
// output is currently being intercepted. Without this, a log line emitted while any app
|
||||||
// instance has its stdout captured would vanish from the console entirely instead of
|
// instance has its stdout captured would vanish from the console entirely instead of
|
||||||
// just also being visible to the capturing parent.
|
// just also being visible to the capturing parent.
|
||||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
real_write(fd, buffer, size);
|
||||||
__real_write(fd, buffer, size);
|
|
||||||
#else
|
|
||||||
::write(fd, buffer, size);
|
|
||||||
#endif
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if (table != nullptr && app_fd_table_is_app_owned(table, fd)) {
|
if (table != nullptr && app_fd_table_is_app_owned(table, fd)) {
|
||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
return real_write(fd, buffer, size);
|
||||||
return __real_write(fd, buffer, size);
|
|
||||||
#else
|
|
||||||
return ::write(fd, buffer, size);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int app_io_close(int fd) {
|
int app_io_close(int fd) {
|
||||||
@ -108,11 +125,7 @@ int app_io_close(int fd) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
return real_close(fd);
|
||||||
return __real_close(fd);
|
|
||||||
#else
|
|
||||||
return ::close(fd);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
327
Modules/app-module/source/stdio_wrap.cpp
Normal file
327
Modules/app-module/source/stdio_wrap.cpp
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Paired with -Wl,--wrap= on POSIX (this module's own CMakeLists.txt) and ESP32 (top-level
|
||||||
|
// CMakeLists.txt); self-registered via dyld interpose below on Apple, whose linker doesn't
|
||||||
|
// support --wrap.
|
||||||
|
#include <app/io.h>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
// Newlib's own stdio (fflush()'s buffer-flush path, in particular) calls the reentrant
|
||||||
|
// _read_r/_write_r/_close_r syscall stubs directly, not the plain read()/write()/close() newlib
|
||||||
|
// itself provides as thin wrappers around them (see e.g. esp-idf's components/newlib/src/
|
||||||
|
// syscalls.c: `write(fd, dst, size) { return _write_r(__getreent(), fd, dst, size); }`)
|
||||||
|
// Wrapping the plain names only catches direct write()-style callers, not newlib's own internal
|
||||||
|
// stdio calls, so the _r stubs are wrapped here instead.
|
||||||
|
#include <reent.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
ssize_t __wrap__read_r(struct _reent* r, int fd, void* buffer, size_t size) {
|
||||||
|
(void)r;
|
||||||
|
return app_io_read(fd, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t __wrap__write_r(struct _reent* r, int fd, const void* buffer, size_t size) {
|
||||||
|
(void)r;
|
||||||
|
return app_io_write(fd, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap__close_r(struct _reent* r, int fd) {
|
||||||
|
(void)r;
|
||||||
|
return app_io_close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
ssize_t __wrap_read(int fd, void* buffer, size_t size) {
|
||||||
|
return app_io_read(fd, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t __wrap_write(int fd, const void* buffer, size_t size) {
|
||||||
|
return app_io_write(fd, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_close(int fd) {
|
||||||
|
return app_io_close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
|
||||||
|
// --wrap also synthesizes __real_read/write/close automatically; dyld interpose doesn't, so
|
||||||
|
// io.cpp's TT_APP_IO_WRAPS_STDIO fallback needs them defined here. dlsym(RTLD_NEXT, ...) is the
|
||||||
|
// standard way to reach the true libSystem implementation despite the interpose below: a direct
|
||||||
|
// call to read/write/close from this file would just recurse into __wrap_read/write/close, since
|
||||||
|
// interpose rewrites every reference to those symbols in the process, this file included.
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
ssize_t __real_read(int fd, void* buffer, size_t size) {
|
||||||
|
static auto real = reinterpret_cast<ssize_t (*)(int, void*, size_t)>(dlsym(RTLD_NEXT, "read"));
|
||||||
|
return real(fd, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t __real_write(int fd, const void* buffer, size_t size) {
|
||||||
|
static auto real = reinterpret_cast<ssize_t (*)(int, const void*, size_t)>(dlsym(RTLD_NEXT, "write"));
|
||||||
|
return real(fd, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __real_close(int fd) {
|
||||||
|
static auto real = reinterpret_cast<int (*)(int)>(dlsym(RTLD_NEXT, "close"));
|
||||||
|
return real(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// <mach-o/dyld-interposing.h> isn't a public SDK header (it ships with dyld's own source, not
|
||||||
|
// Xcode/Command Line Tools), so this reimplements its DYLD_INTERPOSE macro locally; reused below
|
||||||
|
// for the printf-family interposes too.
|
||||||
|
#define TT_DYLD_INTERPOSE(replacement, replacee) \
|
||||||
|
__attribute__((used)) static struct { const void* replacement; const void* replacee; } \
|
||||||
|
tt_interpose_##replacee __attribute__((section("__DATA,__interpose"))) = { \
|
||||||
|
(const void*)(unsigned long)&(replacement), (const void*)(unsigned long)&(replacee) \
|
||||||
|
};
|
||||||
|
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_read, read)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_write, write)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_close, close)
|
||||||
|
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
// region glibc stdio wraps
|
||||||
|
//
|
||||||
|
// libc's printf/fprintf/etc are compiled into the C library and call an internal, non-exported
|
||||||
|
// write() alias, which --wrap=write/the read/write/close interpose above can't reach: only calls
|
||||||
|
// WE make to the public symbol. These wraps instead redirect calls WE make to printf/fprintf/etc,
|
||||||
|
// the same trick as read/write/close above. Newlib (ESP-IDF) doesn't have this gap: its stdio does
|
||||||
|
// call the wrappable syscall stubs, so this block is POSIX-only.
|
||||||
|
//
|
||||||
|
// Scoped to the printf/getc families only: fread/fwrite take an arbitrary FILE* and are already
|
||||||
|
// used sitewide for real file I/O (e.g. File.cpp's readBinaryInternal), so wrapping them would
|
||||||
|
// route every such call through this file's stdin/stdout check, a correctness risk for unrelated
|
||||||
|
// code that isn't worth taking here. putc/getc are excluded too since libc defines them as
|
||||||
|
// macros, not real calls, so wrapping those symbols wouldn't reliably intercept them.
|
||||||
|
|
||||||
|
#if !defined(ESP_PLATFORM)
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
int __real_vfprintf(FILE* stream, const char* format, va_list args);
|
||||||
|
int __real_fputs(const char* s, FILE* stream);
|
||||||
|
int __real_fputc(int c, FILE* stream);
|
||||||
|
int __real_fgetc(FILE* stream);
|
||||||
|
char* __real_fgets(char* buffer, int size, FILE* stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
|
||||||
|
// --wrap synthesizes these automatically elsewhere; on Apple they're defined here via
|
||||||
|
// dlsym(RTLD_NEXT, ...) instead. See the read/write/close __real_* block above for why.
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int __real_vfprintf(FILE* stream, const char* format, va_list args) {
|
||||||
|
static auto real = reinterpret_cast<int (*)(FILE*, const char*, va_list)>(dlsym(RTLD_NEXT, "vfprintf"));
|
||||||
|
return real(stream, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __real_fputs(const char* s, FILE* stream) {
|
||||||
|
static auto real = reinterpret_cast<int (*)(const char*, FILE*)>(dlsym(RTLD_NEXT, "fputs"));
|
||||||
|
return real(s, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __real_fputc(int c, FILE* stream) {
|
||||||
|
static auto real = reinterpret_cast<int (*)(int, FILE*)>(dlsym(RTLD_NEXT, "fputc"));
|
||||||
|
return real(c, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __real_fgetc(FILE* stream) {
|
||||||
|
static auto real = reinterpret_cast<int (*)(FILE*)>(dlsym(RTLD_NEXT, "fgetc"));
|
||||||
|
return real(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* __real_fgets(char* buffer, int size, FILE* stream) {
|
||||||
|
static auto real = reinterpret_cast<char* (*)(char*, int, FILE*)>(dlsym(RTLD_NEXT, "fgets"));
|
||||||
|
return real(buffer, size, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void writeAllToStdout(const void* data, size_t size) {
|
||||||
|
const auto* bytes = static_cast<const char*>(data);
|
||||||
|
size_t remaining = size;
|
||||||
|
while (remaining > 0) {
|
||||||
|
ssize_t written = app_io_write(STDOUT_FILENO, bytes, remaining);
|
||||||
|
if (written <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytes += written;
|
||||||
|
remaining -= static_cast<size_t>(written);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats into stdout via app_io_write() rather than through a FILE*'s own buffering, since that
|
||||||
|
// buffering is exactly what glibc's internal write() call sidesteps --wrap for in the first place.
|
||||||
|
int formatToStdout(const char* format, va_list args) {
|
||||||
|
char stackBuffer[256];
|
||||||
|
va_list argsForStack;
|
||||||
|
va_copy(argsForStack, args);
|
||||||
|
int needed = vsnprintf(stackBuffer, sizeof(stackBuffer), format, argsForStack);
|
||||||
|
va_end(argsForStack);
|
||||||
|
if (needed < 0) {
|
||||||
|
return needed;
|
||||||
|
}
|
||||||
|
if (static_cast<size_t>(needed) < sizeof(stackBuffer)) {
|
||||||
|
writeAllToStdout(stackBuffer, static_cast<size_t>(needed));
|
||||||
|
return needed;
|
||||||
|
}
|
||||||
|
auto heapBuffer = std::make_unique<char[]>(static_cast<size_t>(needed) + 1);
|
||||||
|
va_list argsForHeap;
|
||||||
|
va_copy(argsForHeap, args);
|
||||||
|
vsnprintf(heapBuffer.get(), static_cast<size_t>(needed) + 1, format, argsForHeap);
|
||||||
|
va_end(argsForHeap);
|
||||||
|
writeAllToStdout(heapBuffer.get(), static_cast<size_t>(needed));
|
||||||
|
return needed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int readOneFromStdin(char& out) {
|
||||||
|
return static_cast<int>(app_io_read(STDIN_FILENO, &out, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int __wrap_vprintf(const char* format, va_list args) {
|
||||||
|
return formatToStdout(format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_printf(const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
int result = formatToStdout(format, args);
|
||||||
|
va_end(args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_vfprintf(FILE* stream, const char* format, va_list args) {
|
||||||
|
if (stream == stdout) {
|
||||||
|
return formatToStdout(format, args);
|
||||||
|
}
|
||||||
|
return __real_vfprintf(stream, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_fprintf(FILE* stream, const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
int result = (stream == stdout) ? formatToStdout(format, args) : __real_vfprintf(stream, format, args);
|
||||||
|
va_end(args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_puts(const char* s) {
|
||||||
|
writeAllToStdout(s, strlen(s));
|
||||||
|
writeAllToStdout("\n", 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_fputs(const char* s, FILE* stream) {
|
||||||
|
if (stream == stdout) {
|
||||||
|
writeAllToStdout(s, strlen(s));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return __real_fputs(s, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_putchar(int c) {
|
||||||
|
auto ch = static_cast<char>(c);
|
||||||
|
writeAllToStdout(&ch, 1);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_fputc(int c, FILE* stream) {
|
||||||
|
if (stream == stdout) {
|
||||||
|
return __wrap_putchar(c);
|
||||||
|
}
|
||||||
|
return __real_fputc(c, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_getchar() {
|
||||||
|
char c;
|
||||||
|
return readOneFromStdin(c) == 1 ? static_cast<unsigned char>(c) : EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __wrap_fgetc(FILE* stream) {
|
||||||
|
if (stream == stdin) {
|
||||||
|
return __wrap_getchar();
|
||||||
|
}
|
||||||
|
return __real_fgetc(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* __wrap_fgets(char* buffer, int size, FILE* stream) {
|
||||||
|
if (stream != stdin) {
|
||||||
|
return __real_fgets(buffer, size, stream);
|
||||||
|
}
|
||||||
|
if (size <= 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
for (; i < size - 1; ++i) {
|
||||||
|
char c;
|
||||||
|
if (readOneFromStdin(c) != 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buffer[i] = c;
|
||||||
|
if (c == '\n') {
|
||||||
|
++i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
buffer[i] = '\0';
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_vprintf, vprintf)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_printf, printf)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_vfprintf, vfprintf)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_fprintf, fprintf)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_puts, puts)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_fputs, fputs)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_putchar, putchar)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_fputc, fputc)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_getchar, getchar)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_fgetc, fgetc)
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_fgets, fgets)
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
#endif // !ESP_PLATFORM
|
||||||
|
|
||||||
|
// endregion
|
||||||
@ -6,30 +6,12 @@ enable_language(C CXX ASM)
|
|||||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||||
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||||
|
|
||||||
if (NOT APPLE)
|
|
||||||
# Provides __wrap_read/write/close for the -Wl,--wrap= below (see Tactility/CMakeLists.txt
|
|
||||||
# for the canonical pairing of this file with those flags).
|
|
||||||
target_sources(AppModuleTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../Tactility/Source/AppStdioWrap.cpp)
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC} ${CMAKE_CURRENT_LIST_DIR}/../private)
|
target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC} ${CMAKE_CURRENT_LIST_DIR}/../private)
|
||||||
|
|
||||||
add_test(NAME AppModuleTests COMMAND AppModuleTests)
|
add_test(NAME AppModuleTests COMMAND AppModuleTests)
|
||||||
|
|
||||||
# Matches app-module's own TT_APP_IO_WRAPS_STDIO (see Modules/app-module/CMakeLists.txt):
|
# No --wrap flags or stdio_wrap.cpp source needed here: linking app-module below already brings
|
||||||
# io.cpp calls __real_read/write/close() on non-Apple POSIX, so any final link of it needs
|
# both along (see its own CMakeLists.txt).
|
||||||
# these wraps too, or those symbols go unresolved. The printf-family/getc-family flags are
|
|
||||||
# needed for the same reason: AppStdioWrap.cpp compiles those __wrap_* functions unconditionally
|
|
||||||
# on this platform (see its own #if guard), and they reference __real_vfprintf/fputs/fputc/
|
|
||||||
# fgetc/fgets - those only exist once the matching --wrap flag is passed.
|
|
||||||
if (NOT APPLE)
|
|
||||||
target_link_options(AppModuleTests PRIVATE
|
|
||||||
"-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
|
|
||||||
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
|
||||||
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
|
||||||
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
|
||||||
)
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
target_link_libraries(AppModuleTests PUBLIC
|
target_link_libraries(AppModuleTests PUBLIC
|
||||||
TactilityKernel
|
TactilityKernel
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
#include <tactility/delay.h>
|
#include <tactility/delay.h>
|
||||||
#include <tactility/freertos/task.h>
|
#include <tactility/freertos/task.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -110,6 +111,11 @@ int32_t stream_writer_app_main(int, char*[]) {
|
|||||||
return 7;
|
return 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t printf_stream_writer_app_main(int, char*[]) {
|
||||||
|
printf("loc");
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
// Writes argv[0] to its own stdout, for the app_execute_with_streams() argv-delivery test.
|
// Writes argv[0] to its own stdout, for the app_execute_with_streams() argv-delivery test.
|
||||||
int32_t argv_echo_app_main(int argc, char* argv[]) {
|
int32_t argv_echo_app_main(int argc, char* argv[]) {
|
||||||
if (argc < 1) {
|
if (argc < 1) {
|
||||||
@ -321,3 +327,53 @@ TEST_CASE("app_execute_for_result_with_streams delivers both the stream data and
|
|||||||
app_manager_stop(parent_id);
|
app_manager_stop(parent_id);
|
||||||
app_manager_remove("test.app.execute.parent_streams");
|
app_manager_remove("test.app.execute.parent_streams");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() calls too") {
|
||||||
|
ensure_memory_loader_registered();
|
||||||
|
|
||||||
|
AppManifest parent_manifest { "test.app.execute.printf_parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(location_app_main) } };
|
||||||
|
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||||
|
|
||||||
|
uint32_t parent_id = 0;
|
||||||
|
REQUIRE_EQ(app_start("test.app.execute.printf_parent", 0, nullptr, &parent_id), ERROR_NONE);
|
||||||
|
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||||
|
|
||||||
|
TaskEventGroup parent_event_group {};
|
||||||
|
task_event_group_construct(&parent_event_group);
|
||||||
|
AppEventSubscription parent_sub {};
|
||||||
|
REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE);
|
||||||
|
|
||||||
|
uint8_t storage[64];
|
||||||
|
AppStream child_stdout {};
|
||||||
|
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &parent_event_group };
|
||||||
|
|
||||||
|
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(printf_stream_writer_app_main) };
|
||||||
|
uint32_t child_id = 0;
|
||||||
|
REQUIRE_EQ(app_execute_for_result_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, parent_id, &child_id), ERROR_NONE);
|
||||||
|
|
||||||
|
std::vector<uint8_t> received;
|
||||||
|
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
|
||||||
|
uint8_t chunk[16];
|
||||||
|
size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk));
|
||||||
|
if (n == 0) {
|
||||||
|
break; // EOF
|
||||||
|
}
|
||||||
|
received.insert(received.end(), chunk, chunk + n);
|
||||||
|
}
|
||||||
|
REQUIRE_EQ(received.size(), 3u);
|
||||||
|
CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0);
|
||||||
|
|
||||||
|
REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||||
|
AppEvent event {};
|
||||||
|
REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE);
|
||||||
|
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||||
|
CHECK_EQ(event.result.launch_id, child_id);
|
||||||
|
CHECK_EQ(event.result.result, 7);
|
||||||
|
|
||||||
|
app_stream_unsubscribe(&child_stdout);
|
||||||
|
app_event_unsubscribe(&parent_sub);
|
||||||
|
task_event_group_destruct(&parent_event_group);
|
||||||
|
app_manager_stop(child_id);
|
||||||
|
app_manager_stop(parent_id);
|
||||||
|
app_manager_remove("test.app.execute.printf_parent");
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -66,6 +67,11 @@ int32_t stdout_writer_app_main(int, char*[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t stdout_printf_app_main(int, char*[]) {
|
||||||
|
printf("hello");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
std::atomic<bool> g_blocked_writer_saw_error { false };
|
std::atomic<bool> g_blocked_writer_saw_error { false };
|
||||||
std::atomic<bool> g_blocked_writer_done { false };
|
std::atomic<bool> g_blocked_writer_done { false };
|
||||||
|
|
||||||
@ -180,6 +186,41 @@ TEST_CASE("app_start_with_streams pipes a child's app_io_write() calls into a pa
|
|||||||
app_manager_remove("test.io.writer");
|
app_manager_remove("test.io.writer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("app_start_with_streams pipes a child's plain printf() calls into a parent-owned AppStream") {
|
||||||
|
ensure_memory_loader_registered();
|
||||||
|
|
||||||
|
AppManifest manifest { "test.io.printf_writer", "PrintfWriter", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(stdout_printf_app_main) } };
|
||||||
|
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
|
uint8_t storage[64];
|
||||||
|
AppStream child_stdout {};
|
||||||
|
|
||||||
|
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
|
||||||
|
AppInstanceId child_id = 0;
|
||||||
|
REQUIRE_EQ(app_start_with_streams("test.io.printf_writer", &binding, 1, &child_id), ERROR_NONE);
|
||||||
|
|
||||||
|
std::vector<uint8_t> received;
|
||||||
|
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
|
||||||
|
uint8_t chunk[16];
|
||||||
|
size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk));
|
||||||
|
if (n == 0) {
|
||||||
|
break; // EOF
|
||||||
|
}
|
||||||
|
received.insert(received.end(), chunk, chunk + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE_EQ(received.size(), 5u);
|
||||||
|
CHECK_EQ(std::memcmp(received.data(), "hello", 5), 0);
|
||||||
|
|
||||||
|
REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000));
|
||||||
|
app_stream_unsubscribe(&child_stdout);
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
app_manager_remove("test.io.printf_writer");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("a write blocked on a full stream wakes with an error once the consumer closes it") {
|
TEST_CASE("a write blocked on a full stream wakes with an error once the consumer closes it") {
|
||||||
ensure_memory_loader_registered();
|
ensure_memory_loader_registered();
|
||||||
g_blocked_writer_saw_error.store(false, std::memory_order_relaxed);
|
g_blocked_writer_saw_error.store(false, std::memory_order_relaxed);
|
||||||
|
|||||||
@ -27,7 +27,6 @@ constexpr auto HID_PROC_TASK_STACK = 4096;
|
|||||||
constexpr auto HID_PROC_TASK_PRIORITY = 5;
|
constexpr auto HID_PROC_TASK_PRIORITY = 5;
|
||||||
constexpr auto HID_STOP_TIMEOUT_MS = 2000;
|
constexpr auto HID_STOP_TIMEOUT_MS = 2000;
|
||||||
constexpr auto MAX_SUBSCRIBERS = 4;
|
constexpr auto MAX_SUBSCRIBERS = 4;
|
||||||
constexpr auto USB_HID_KB_QUEUE_SIZE = 16;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
hid_host_device_handle_t handle;
|
hid_host_device_handle_t handle;
|
||||||
@ -437,47 +436,9 @@ static const UsbHidApi hid_api = {
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
// region Dynamic KEYBOARD_TYPE device
|
// region Dynamic KEYBOARD_TYPE device
|
||||||
//
|
|
||||||
// While a physical USB keyboard is connected, a KEYBOARD_TYPE child device is constructed so the
|
|
||||||
// rest of the system (lvgl_hardware_keyboard_is_available(), Tactility's KeyboardDeviceListener)
|
|
||||||
// sees a real hardware keyboard through the same generic device model as any other keyboard, e.g.
|
|
||||||
// Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp. Real key events are delivered exclusively
|
|
||||||
// through this device (kb_handle's hid_interface_callback pushes into its queue below); the
|
|
||||||
// generic UsbHidEvent publish/subscribe channel above is unaffected for mouse move/button/scroll
|
|
||||||
// and the right-click-as-ESC synthesis it already does.
|
|
||||||
|
|
||||||
static error_t usb_hid_kb_device_start(Device* device) {
|
|
||||||
auto* queue = xQueueCreate(USB_HID_KB_QUEUE_SIZE, sizeof(KeyboardKeyData));
|
|
||||||
if (queue == nullptr) {
|
|
||||||
return ERROR_RESOURCE;
|
|
||||||
}
|
|
||||||
device_set_driver_data(device, queue);
|
|
||||||
return ERROR_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static error_t usb_hid_kb_device_stop(Device* device) {
|
|
||||||
auto* queue = static_cast<QueueHandle_t>(device_get_driver_data(device));
|
|
||||||
vQueueDelete(queue);
|
|
||||||
device_set_driver_data(device, nullptr);
|
|
||||||
return ERROR_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static error_t usb_hid_kb_device_read_key(Device* device, KeyboardKeyData* data) {
|
|
||||||
auto* queue = static_cast<QueueHandle_t>(device_get_driver_data(device));
|
|
||||||
if (queue == nullptr) {
|
|
||||||
*data = {};
|
|
||||||
return ERROR_NONE;
|
|
||||||
}
|
|
||||||
if (xQueueReceive(queue, data, 0) != pdTRUE) {
|
|
||||||
*data = {};
|
|
||||||
return ERROR_NONE;
|
|
||||||
}
|
|
||||||
data->continue_reading = uxQueueMessagesWaiting(queue) > 0;
|
|
||||||
return ERROR_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const KeyboardApi esp32_usbhost_hid_keyboard_api = {
|
static const KeyboardApi esp32_usbhost_hid_keyboard_api = {
|
||||||
.read_key = usb_hid_kb_device_read_key,
|
.read_key = nullptr,
|
||||||
.get_backlight = nullptr,
|
.get_backlight = nullptr,
|
||||||
.is_present = nullptr,
|
.is_present = nullptr,
|
||||||
};
|
};
|
||||||
@ -485,8 +446,9 @@ static const KeyboardApi esp32_usbhost_hid_keyboard_api = {
|
|||||||
Driver esp32_usbhost_hid_keyboard_driver = {
|
Driver esp32_usbhost_hid_keyboard_driver = {
|
||||||
.name = "esp32_usbhost_hid_keyboard",
|
.name = "esp32_usbhost_hid_keyboard",
|
||||||
.compatible = (const char*[]) { nullptr },
|
.compatible = (const char*[]) { nullptr },
|
||||||
.start_device = usb_hid_kb_device_start,
|
.start_device = nullptr,
|
||||||
.stop_device = usb_hid_kb_device_stop,
|
.stop_device = nullptr,
|
||||||
|
.probe = nullptr,
|
||||||
.api = &esp32_usbhost_hid_keyboard_api,
|
.api = &esp32_usbhost_hid_keyboard_api,
|
||||||
.device_type = &KEYBOARD_TYPE,
|
.device_type = &KEYBOARD_TYPE,
|
||||||
.owner = nullptr,
|
.owner = nullptr,
|
||||||
@ -543,9 +505,8 @@ static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bo
|
|||||||
if (!ctx->kb_device_active) {
|
if (!ctx->kb_device_active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto* queue = static_cast<QueueHandle_t>(device_get_driver_data(&ctx->kb_device));
|
|
||||||
KeyboardKeyData data = { lv_key, pressed, false, ctrl, alt, hid_keycode, hid_modifier };
|
KeyboardKeyData data = { lv_key, pressed, false, ctrl, alt, hid_keycode, hid_modifier };
|
||||||
xQueueSend(queue, &data, 0);
|
keyboard_emit_key(&ctx->kb_device, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|||||||
@ -6,3 +6,12 @@ add_library(platform-posix OBJECT)
|
|||||||
target_sources(platform-posix PRIVATE ${SOURCES})
|
target_sources(platform-posix PRIVATE ${SOURCES})
|
||||||
#target_include_directories(platform-posix PUBLIC include/)
|
#target_include_directories(platform-posix PUBLIC include/)
|
||||||
target_link_libraries(platform-posix PUBLIC TactilityKernel)
|
target_link_libraries(platform-posix PUBLIC TactilityKernel)
|
||||||
|
|
||||||
|
# Routes every pthread_attr_setstack() call to __wrap_pthread_attr_setstack() in source/pthread_stack_wrap.c,
|
||||||
|
# which no-ops it. see that file for why.
|
||||||
|
# --wrap is a GNU ld option; Apple's linker doesn't support it, so that file instead self-registers a dyld interpose
|
||||||
|
# for this symbol on Apple, needing no link flag. PUBLIC so every consumer of this OBJECT library
|
||||||
|
# (Tactility, and every test target that links platform-posix directly) gets it applied too.
|
||||||
|
if (NOT APPLE)
|
||||||
|
target_link_options(platform-posix PUBLIC "-Wl,--wrap=pthread_attr_setstack")
|
||||||
|
endif ()
|
||||||
|
|||||||
@ -2,20 +2,15 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linked in with -Wl,--wrap=pthread_attr_setstack (see Tactility/CMakeLists.txt).
|
* FreeRTOS's POSIX port hands every task a stack carved from its own heap (pvPortMalloc) via this call.
|
||||||
|
* pthread_attr_setstack requires page alignment that pvPortMalloc doesn't guarantee.
|
||||||
|
* When it succeeds anyway, the task's real pthread stack ends up living inside that small FreeRTOS heap region,
|
||||||
|
* where a thread doing heavier stack work (e.g. Mesa GL shader compilation) can silently corrupt adjacent heap_4 objects.
|
||||||
|
* No-op'ing the call instead leaves every task's pthread_attr_t at its pthread_attr_init() default,
|
||||||
|
* so pthread_create() gives it a real, properly sized stack.
|
||||||
*
|
*
|
||||||
* FreeRTOS's POSIX port (Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/port.c)
|
* Linked via -Wl,--wrap=pthread_attr_setstack (this module's own CMakeLists.txt) on non-Apple
|
||||||
* hands every task a stack carved out of its own heap (pvPortMalloc) via this call.
|
* platforms; reused as a dyld interpose target below on Apple platforms, whose linker lacks --wrap.
|
||||||
* pthread_attr_setstack requires page alignment, which pvPortMalloc doesn't guarantee;
|
|
||||||
* when it happens to succeed anyway (allocator alignment can vary run to run), the
|
|
||||||
* task's real pthread stack ends up living inside that small FreeRTOS heap region -
|
|
||||||
* fine for typical embedded task code, but a desktop GL driver doing on-the-fly shader
|
|
||||||
* compilation on that thread (e.g. Mesa on first frame present) can overflow it and
|
|
||||||
* silently corrupt adjacent heap_4 objects.
|
|
||||||
*
|
|
||||||
* Wrapping the call out entirely (rather than patching the vendored port.c) leaves every
|
|
||||||
* task's pthread_attr_t at its pthread_attr_init() default, so pthread_create() always
|
|
||||||
* gives it a real, properly allocated default-size stack instead.
|
|
||||||
*/
|
*/
|
||||||
int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize) {
|
int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize) {
|
||||||
(void)attr;
|
(void)attr;
|
||||||
@ -23,3 +18,17 @@ int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t s
|
|||||||
(void)stacksize;
|
(void)stacksize;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
|
||||||
|
// <mach-o/dyld-interposing.h> isn't a public SDK header (it ships with dyld's own source, not
|
||||||
|
// Xcode/Command Line Tools), so this reimplements its DYLD_INTERPOSE macro locally.
|
||||||
|
#define TT_DYLD_INTERPOSE(replacement, replacee) \
|
||||||
|
__attribute__((used)) static struct { const void* replacement; const void* replacee; } \
|
||||||
|
tt_interpose_##replacee __attribute__((section("__DATA,__interpose"))) = { \
|
||||||
|
(const void*)(unsigned long)&(replacement), (const void*)(unsigned long)&(replacee) \
|
||||||
|
};
|
||||||
|
|
||||||
|
TT_DYLD_INTERPOSE(__wrap_pthread_attr_setstack, pthread_attr_setstack)
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@ -185,31 +185,9 @@ else ()
|
|||||||
# Exports Tactility's own symbols (-rdynamic) so a dlopen()ed app-posix-module app can resolve
|
# Exports Tactility's own symbols (-rdynamic) so a dlopen()ed app-posix-module app can resolve
|
||||||
# calls back into it - the OS-native equivalent of app-esp32-module's custom symbol resolver.
|
# calls back into it - the OS-native equivalent of app-esp32-module's custom symbol resolver.
|
||||||
set_target_properties(Tactility PROPERTIES ENABLE_EXPORTS ON)
|
set_target_properties(Tactility PROPERTIES ENABLE_EXPORTS ON)
|
||||||
# Routes every pthread_attr_setstack() call (FreeRTOS's POSIX port hands each task a stack
|
# pthread_attr_setstack and read/write/close/printf-family aren't wrapped here: linking
|
||||||
# carved out of its own heap through this) to __wrap_pthread_attr_setstack() in
|
# platform-posix and app-module already brings those wraps along (see their own
|
||||||
# Platforms/platform-posix/source/pthread_stack_wrap.c, which no-ops it - see that file for why.
|
# CMakeLists.txt files).
|
||||||
# --wrap is a GNU ld option; Apple's linker doesn't support it.
|
|
||||||
if (NOT APPLE)
|
|
||||||
target_link_options(Tactility PRIVATE "-Wl,--wrap=pthread_attr_setstack")
|
|
||||||
# Routes every read()/write()/close() call to Tactility/Source/AppStdioWrap.cpp's
|
|
||||||
# __wrap_read/write/close(), which forward into app_io_read/write/close() - the fd-table
|
|
||||||
# dispatch that lets an app's own stdio (e.g. a fileselection dialog's printf'd result
|
|
||||||
# path) reach an AppStream a parent bound via app_manager_start_with_streams(). Mirrors
|
|
||||||
# what ESP-IDF's build already does for the ESP32 target (see top-level CMakeLists.txt).
|
|
||||||
target_link_options(Tactility PRIVATE "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close")
|
|
||||||
# glibc's printf/fprintf/etc don't call the public write() symbol internally (they're
|
|
||||||
# already compiled into libc.so, out of --wrap's reach), so the read/write/close wrap
|
|
||||||
# above can't see them. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the
|
|
||||||
# wrappable syscall stubs - so this block is POSIX-only. Wrapping these symbols instead
|
|
||||||
# redirects OUR OWN calls to them (the only ones --wrap can rewrite) through
|
|
||||||
# AppStdioWrap.cpp's __wrap_* functions, which check the target stream (stdout/stdin) and
|
|
||||||
# fall back to the real libc function for any other FILE*.
|
|
||||||
target_link_options(Tactility PRIVATE
|
|
||||||
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
|
||||||
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
|
||||||
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
|
||||||
)
|
|
||||||
endif ()
|
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,200 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
// Paired with -Wl,--wrap=read/write/close - see Tactility/CMakeLists.txt (POSIX) and the
|
|
||||||
// top-level CMakeLists.txt (ESP32) for where that's applied. On a platform where it isn't
|
|
||||||
// (currently: macOS, whose linker doesn't support --wrap), these are simply never called - real
|
|
||||||
// read()/write()/close() calls go straight through unredirected.
|
|
||||||
#include <app/io.h>
|
|
||||||
|
|
||||||
#include <sys/types.h>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
ssize_t __wrap_read(int fd, void* buffer, size_t size) {
|
|
||||||
return app_io_read(fd, buffer, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
ssize_t __wrap_write(int fd, const void* buffer, size_t size) {
|
|
||||||
return app_io_write(fd, buffer, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_close(int fd) {
|
|
||||||
return app_io_close(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// region glibc stdio wraps
|
|
||||||
//
|
|
||||||
// glibc's printf/fprintf/etc are compiled into libc.so and call an internal, non-exported write()
|
|
||||||
// alias - --wrap=write (above) can't reach that call, only calls WE make to the public symbol.
|
|
||||||
// These wraps instead redirect calls WE make to printf/fprintf/etc, the same trick as read/write/
|
|
||||||
// close above. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the wrappable syscall
|
|
||||||
// stubs - so Tactility/CMakeLists.txt only applies the matching -Wl,--wrap= flags on POSIX.
|
|
||||||
//
|
|
||||||
// Scoped to the printf/getc families only: fread/fwrite take an arbitrary FILE* and are already
|
|
||||||
// used sitewide for real file I/O (e.g. File.cpp's readBinaryInternal), so wrapping them would
|
|
||||||
// route every such call through this file's stdin/stdout check - a correctness risk for unrelated
|
|
||||||
// code that isn't worth taking here. putc/getc are excluded too since glibc defines them as
|
|
||||||
// macros, not real calls, so wrapping those symbols wouldn't reliably intercept them.
|
|
||||||
|
|
||||||
#if !defined(ESP_PLATFORM) && !defined(__APPLE__)
|
|
||||||
|
|
||||||
#include <cstdarg>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
|
||||||
#include <memory>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
int __real_vfprintf(FILE* stream, const char* format, va_list args);
|
|
||||||
int __real_fputs(const char* s, FILE* stream);
|
|
||||||
int __real_fputc(int c, FILE* stream);
|
|
||||||
int __real_fgetc(FILE* stream);
|
|
||||||
char* __real_fgets(char* buffer, int size, FILE* stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void writeAllToStdout(const void* data, size_t size) {
|
|
||||||
const auto* bytes = static_cast<const char*>(data);
|
|
||||||
size_t remaining = size;
|
|
||||||
while (remaining > 0) {
|
|
||||||
ssize_t written = app_io_write(STDOUT_FILENO, bytes, remaining);
|
|
||||||
if (written <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
bytes += written;
|
|
||||||
remaining -= static_cast<size_t>(written);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formats into stdout via app_io_write() rather than through a FILE*'s own buffering, since that
|
|
||||||
// buffering is exactly what glibc's internal write() call sidesteps --wrap for in the first place.
|
|
||||||
int formatToStdout(const char* format, va_list args) {
|
|
||||||
char stackBuffer[256];
|
|
||||||
va_list argsForStack;
|
|
||||||
va_copy(argsForStack, args);
|
|
||||||
int needed = vsnprintf(stackBuffer, sizeof(stackBuffer), format, argsForStack);
|
|
||||||
va_end(argsForStack);
|
|
||||||
if (needed < 0) {
|
|
||||||
return needed;
|
|
||||||
}
|
|
||||||
if (static_cast<size_t>(needed) < sizeof(stackBuffer)) {
|
|
||||||
writeAllToStdout(stackBuffer, static_cast<size_t>(needed));
|
|
||||||
return needed;
|
|
||||||
}
|
|
||||||
auto heapBuffer = std::make_unique<char[]>(static_cast<size_t>(needed) + 1);
|
|
||||||
va_list argsForHeap;
|
|
||||||
va_copy(argsForHeap, args);
|
|
||||||
vsnprintf(heapBuffer.get(), static_cast<size_t>(needed) + 1, format, argsForHeap);
|
|
||||||
va_end(argsForHeap);
|
|
||||||
writeAllToStdout(heapBuffer.get(), static_cast<size_t>(needed));
|
|
||||||
return needed;
|
|
||||||
}
|
|
||||||
|
|
||||||
int readOneFromStdin(char& out) {
|
|
||||||
return static_cast<int>(app_io_read(STDIN_FILENO, &out, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
int __wrap_vprintf(const char* format, va_list args) {
|
|
||||||
return formatToStdout(format, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_printf(const char* format, ...) {
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
int result = formatToStdout(format, args);
|
|
||||||
va_end(args);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_vfprintf(FILE* stream, const char* format, va_list args) {
|
|
||||||
if (stream == stdout) {
|
|
||||||
return formatToStdout(format, args);
|
|
||||||
}
|
|
||||||
return __real_vfprintf(stream, format, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_fprintf(FILE* stream, const char* format, ...) {
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
int result = (stream == stdout) ? formatToStdout(format, args) : __real_vfprintf(stream, format, args);
|
|
||||||
va_end(args);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_puts(const char* s) {
|
|
||||||
writeAllToStdout(s, strlen(s));
|
|
||||||
writeAllToStdout("\n", 1);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_fputs(const char* s, FILE* stream) {
|
|
||||||
if (stream == stdout) {
|
|
||||||
writeAllToStdout(s, strlen(s));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return __real_fputs(s, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_putchar(int c) {
|
|
||||||
auto ch = static_cast<char>(c);
|
|
||||||
writeAllToStdout(&ch, 1);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_fputc(int c, FILE* stream) {
|
|
||||||
if (stream == stdout) {
|
|
||||||
return __wrap_putchar(c);
|
|
||||||
}
|
|
||||||
return __real_fputc(c, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_getchar() {
|
|
||||||
char c;
|
|
||||||
return readOneFromStdin(c) == 1 ? static_cast<unsigned char>(c) : EOF;
|
|
||||||
}
|
|
||||||
|
|
||||||
int __wrap_fgetc(FILE* stream) {
|
|
||||||
if (stream == stdin) {
|
|
||||||
return __wrap_getchar();
|
|
||||||
}
|
|
||||||
return __real_fgetc(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* __wrap_fgets(char* buffer, int size, FILE* stream) {
|
|
||||||
if (stream != stdin) {
|
|
||||||
return __real_fgets(buffer, size, stream);
|
|
||||||
}
|
|
||||||
if (size <= 0) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
int i = 0;
|
|
||||||
for (; i < size - 1; ++i) {
|
|
||||||
char c;
|
|
||||||
if (readOneFromStdin(c) != 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buffer[i] = c;
|
|
||||||
if (c == '\n') {
|
|
||||||
++i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i == 0) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
buffer[i] = '\0';
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // !ESP_PLATFORM && !__APPLE__
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
@ -85,7 +85,7 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
|
|
||||||
if (ctx.resultCode == 0) {
|
if (ctx.resultCode == 0) {
|
||||||
// The parent captures this via an AppStream bound to our stdout (see startWithMode()) -
|
// The parent captures this via an AppStream bound to our stdout (see startWithMode()) -
|
||||||
// see AppStdioWrap.cpp for how printf() itself gets routed there on POSIX.
|
// see Modules/app-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX.
|
||||||
LOG_I(TAG, "Result: %s", ctx.resultPath.c_str());
|
LOG_I(TAG, "Result: %s", ctx.resultPath.c_str());
|
||||||
printf("%s", ctx.resultPath.c_str());
|
printf("%s", ctx.resultPath.c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,7 +138,7 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
|
|
||||||
if (ctx.resultCode == 0) {
|
if (ctx.resultCode == 0) {
|
||||||
// The caller captures this via an AppStream bound to our stdout (see start()); see
|
// The caller captures this via an AppStream bound to our stdout (see start()); see
|
||||||
// AppStdioWrap.cpp for how printf() itself gets routed there on POSIX.
|
// Modules/app-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX.
|
||||||
printf("%s", ctx.resultText.c_str());
|
printf("%s", ctx.resultText.c_str());
|
||||||
}
|
}
|
||||||
return ctx.resultCode;
|
return ctx.resultCode;
|
||||||
|
|||||||
@ -19,18 +19,9 @@ target_include_directories(TactilityTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../P
|
|||||||
|
|
||||||
add_test(NAME TactilityTests COMMAND TactilityTests)
|
add_test(NAME TactilityTests COMMAND TactilityTests)
|
||||||
|
|
||||||
# Matches Tactility/CMakeLists.txt's own set of --wrap flags: this binary also compiles
|
# No --wrap flags declared here: linking platform-posix and app-module below already brings all
|
||||||
# AppStdioWrap.cpp and links app-module (whose io.cpp calls __real_read/write/close() on
|
# of pthread_attr_setstack/read/write/close/the printf family along (see their own CMakeLists.txt
|
||||||
# non-Apple POSIX - see TT_APP_IO_WRAPS_STDIO in Modules/app-module/CMakeLists.txt), so it
|
# files).
|
||||||
# needs the same wraps applied or those go unresolved.
|
|
||||||
if (NOT APPLE)
|
|
||||||
target_link_options(TactilityTests PRIVATE
|
|
||||||
"-Wl,--wrap=pthread_attr_setstack" "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
|
|
||||||
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
|
||||||
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
|
||||||
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
|
||||||
)
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
target_link_libraries(TactilityTests PRIVATE
|
target_link_libraries(TactilityTests PRIVATE
|
||||||
TactilityKernel
|
TactilityKernel
|
||||||
|
|||||||
@ -35,7 +35,7 @@ typedef uint8_t device_flags_t;
|
|||||||
|
|
||||||
#define DEVICE_FLAG_VIRTUAL BIT(2) /* No physical hardware */
|
#define DEVICE_FLAG_VIRTUAL BIT(2) /* No physical hardware */
|
||||||
#define DEVICE_FLAG_REMOVABLE BIT(3) /* May disappear (USB, SDIO, etc.) */
|
#define DEVICE_FLAG_REMOVABLE BIT(3) /* May disappear (USB, SDIO, etc.) */
|
||||||
#define DEVICE_FLAG_HOTPLUG BIT(4) /* Supports hotplug */
|
#define DEVICE_FLAG_HOTPLUG BIT(4) /* Automatically set by device_hotplug_register() when the driver has a probe */
|
||||||
|
|
||||||
/** Represents a piece of hardware */
|
/** Represents a piece of hardware */
|
||||||
struct Device {
|
struct Device {
|
||||||
@ -113,6 +113,7 @@ error_t device_remove(struct Device* device);
|
|||||||
* device's lock for its entire body, so doing so would deadlock.
|
* device's lock for its entire body, so doing so would deadlock.
|
||||||
* @param[in,out] device non-null device pointer
|
* @param[in,out] device non-null device pointer
|
||||||
* @retval ERROR_INVALID_STATE if the device is already started or not added
|
* @retval ERROR_INVALID_STATE if the device is already started or not added
|
||||||
|
* @retval ERROR_NOT_FOUND probe() reported the device isn't present
|
||||||
* @retval ERROR_RESOURCE when driver binding fails
|
* @retval ERROR_RESOURCE when driver binding fails
|
||||||
* @retval ERROR_NONE on success
|
* @retval ERROR_NONE on success
|
||||||
*/
|
*/
|
||||||
@ -389,6 +390,37 @@ bool device_has_active_by_type(const struct DeviceType* type);
|
|||||||
*/
|
*/
|
||||||
error_t device_get_first_by_compatible(const char* compatible, struct Device** out_device);
|
error_t device_get_first_by_compatible(const char* compatible, struct Device** out_device);
|
||||||
|
|
||||||
|
/** Consecutive device_hotplug_poll_once() calls a probe() result must hold before it takes effect. */
|
||||||
|
#define DEVICE_HOTPLUG_DEBOUNCE_COUNT 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers @a device with the hotplug poller (see device_hotplug_poll_once()): started once
|
||||||
|
* probe() (or immediately if NULL) reports present, stopped when it reports absent. Sets
|
||||||
|
* DEVICE_FLAG_HOTPLUG on @a device if its driver has a `probe`. A device never registered here
|
||||||
|
* is never touched by the poller. No-op if already registered. Safe to call concurrently with
|
||||||
|
* device_hotplug_poll_once() and with device_hotplug_unregister().
|
||||||
|
*/
|
||||||
|
void device_hotplug_register(struct Device* device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverses device_hotplug_register(), clearing DEVICE_FLAG_HOTPLUG. No-op if not registered.
|
||||||
|
* Safe to call concurrently with device_hotplug_poll_once(); call this before
|
||||||
|
* device_remove()/device_destruct() if a registered device is being permanently torn down.
|
||||||
|
*/
|
||||||
|
void device_hotplug_unregister(struct Device* device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts/stops every device_hotplug_register()'d device per its driver's probe() (or
|
||||||
|
* unconditionally, once, if probe is NULL), debounced across DEVICE_HOTPLUG_DEBOUNCE_COUNT
|
||||||
|
* calls. Never removes/destructs a device.
|
||||||
|
* @retval ERROR_NONE on success
|
||||||
|
* @retval (other) a mandatory (probe == NULL) device failed to start
|
||||||
|
*/
|
||||||
|
error_t device_hotplug_poll_once(void);
|
||||||
|
|
||||||
|
/** Starts a periodic timer that calls device_hotplug_poll_once(). */
|
||||||
|
void device_hotplug_start_polling(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -23,6 +23,14 @@ struct Driver {
|
|||||||
error_t (*start_device)(struct Device* dev);
|
error_t (*start_device)(struct Device* dev);
|
||||||
/** Function to deinitialize the driver for a device */
|
/** Function to deinitialize the driver for a device */
|
||||||
error_t (*stop_device)(struct Device* dev);
|
error_t (*stop_device)(struct Device* dev);
|
||||||
|
/**
|
||||||
|
* Optional: reports whether the hardware is physically present. Checked by driver_bind() and periodically by device_hotplug_poll_once().
|
||||||
|
* NULL means always present.
|
||||||
|
* @retval ERROR_NONE the hardware is present
|
||||||
|
* @retval (other) not present (or the probe itself failed - treated the same)
|
||||||
|
* @warning Must not call device_add()/device_remove() - may run while the ledger is locked.
|
||||||
|
*/
|
||||||
|
error_t (*probe)(struct Device* device);
|
||||||
/** Contains the driver's functions */
|
/** Contains the driver's functions */
|
||||||
const void* api;
|
const void* api;
|
||||||
/** Which type of devices this driver creates (can be NULL) */
|
/** Which type of devices this driver creates (can be NULL) */
|
||||||
@ -100,6 +108,7 @@ error_t driver_remove_destruct(struct Driver* driver);
|
|||||||
*
|
*
|
||||||
* @param driver The driver to bind.
|
* @param driver The driver to bind.
|
||||||
* @param device The device to bind to.
|
* @param device The device to bind to.
|
||||||
|
* @retval ERROR_NOT_FOUND probe() reported the device isn't present
|
||||||
* @return ERROR_NONE if successful, or an error code otherwise.
|
* @return ERROR_NONE if successful, or an error code otherwise.
|
||||||
*/
|
*/
|
||||||
error_t driver_bind(struct Driver* driver, struct Device* device);
|
error_t driver_bind(struct Driver* driver, struct Device* device);
|
||||||
|
|||||||
@ -14,12 +14,9 @@ extern "C" {
|
|||||||
/**
|
/**
|
||||||
* @brief Named Unicode codepoints for KeyboardKeyData::key.
|
* @brief Named Unicode codepoints for KeyboardKeyData::key.
|
||||||
*
|
*
|
||||||
* Keys that produce an ordinary character use that character's own codepoint directly (e.g. 'a',
|
* Some common codepoints.
|
||||||
* ' ') and don't need a name here. These are the standard Unicode symbol codepoints used instead
|
* There are no official codepoints for arrow keys, so we use the symbols as placeholder.
|
||||||
* of LVGL's LV_KEY_* sentinels for keys with no character of their own (arrows, Home/End,
|
* Subsystems like LVGL can translate these special codepoints to specific actions.
|
||||||
* Tab/Shift+Tab-as-focus-navigation), plus names for the handful of C0 control keys every driver
|
|
||||||
* needs (Enter/Escape/Backspace/Delete/Tab). Modules/lvgl-module/source/devices/keyboard.cpp
|
|
||||||
* translates these back into LV_KEY_* internally for LVGL.
|
|
||||||
*/
|
*/
|
||||||
typedef enum {
|
typedef enum {
|
||||||
CODEPOINT_ENTER = '\r',
|
CODEPOINT_ENTER = '\r',
|
||||||
@ -77,11 +74,11 @@ struct KeyboardKeyData {
|
|||||||
bool alt;
|
bool alt;
|
||||||
/**
|
/**
|
||||||
* @brief Standard USB HID keyboard usage code for this key (USB HID Usage Tables, page 0x07),
|
* @brief Standard USB HID keyboard usage code for this key (USB HID Usage Tables, page 0x07),
|
||||||
* or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers
|
* or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers need).
|
||||||
* need). Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so
|
* Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so consumers that
|
||||||
* consumers that want to mirror physical key presses as real HID reports (e.g. USB HID output)
|
* want to mirror physical key presses as real HID reports (e.g. USB HID output) don't have to
|
||||||
* don't have to reverse-engineer one out of `key`'s codepoint encoding - which is lossy for
|
* reverse-engineer one out of `key`'s codepoint encoding.
|
||||||
* keys with no Unicode/LVGL representation at all, e.g. F1-F12.
|
* Encoding is lossy for keys with no Unicode/LVGL representation at all, e.g. F1-F12.
|
||||||
*/
|
*/
|
||||||
uint8_t hid_keycode;
|
uint8_t hid_keycode;
|
||||||
/**
|
/**
|
||||||
@ -91,12 +88,35 @@ struct KeyboardKeyData {
|
|||||||
uint8_t hid_modifier;
|
uint8_t hid_modifier;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of key events that can be queued per subscription in the default
|
||||||
|
* subscribe/unsubscribe/poll implementation before older subscribers' fanned-out events start
|
||||||
|
* being dropped for that subscriber (silently, preserving FIFO order of what's already queued).
|
||||||
|
*/
|
||||||
|
#define KEYBOARD_EVENT_QUEUE_CAPACITY 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Caller-owned subscription node for keyboard_subscribe()/keyboard_unsubscribe()/
|
||||||
|
* keyboard_poll().
|
||||||
|
* @warning `internal` is private to keyboard.cpp; do not read or write it directly.
|
||||||
|
*/
|
||||||
|
struct KeyboardEventSubscription {
|
||||||
|
struct {
|
||||||
|
struct Device* device;
|
||||||
|
struct KeyboardKeyData queue[KEYBOARD_EVENT_QUEUE_CAPACITY];
|
||||||
|
uint8_t head;
|
||||||
|
uint8_t count;
|
||||||
|
struct KeyboardEventSubscription* next;
|
||||||
|
} internal;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief API for keyboard drivers.
|
* @brief API for keyboard drivers.
|
||||||
*/
|
*/
|
||||||
struct KeyboardApi {
|
struct KeyboardApi {
|
||||||
/**
|
/**
|
||||||
* @brief Reads the next pending key event, if any.
|
* @brief Reads the next pending key event, if any.
|
||||||
|
* May be NULL if the driver instead pushes events itself via keyboard_emit_key() (e.g. from a dedicated interrupt-handling task).
|
||||||
* @param[in] device the keyboard device
|
* @param[in] device the keyboard device
|
||||||
* @param[out] data the key event data
|
* @param[out] data the key event data
|
||||||
* @retval ERROR_NONE when the operation was successful
|
* @retval ERROR_NONE when the operation was successful
|
||||||
@ -104,7 +124,7 @@ struct KeyboardApi {
|
|||||||
error_t (*read_key)(struct Device* device, struct KeyboardKeyData* data);
|
error_t (*read_key)(struct Device* device, struct KeyboardKeyData* data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the baclight if the keyboard has one.
|
* @brief Returns the backlight if the keyboard has one.
|
||||||
* @warning Returns a referenced device. Must call device_put() afterwards.
|
* @warning Returns a referenced device. Must call device_put() afterwards.
|
||||||
* @param[in] device the keyboard device
|
* @param[in] device the keyboard device
|
||||||
* @param[out] backlight_device the output backlight device
|
* @param[out] backlight_device the output backlight device
|
||||||
@ -114,22 +134,34 @@ struct KeyboardApi {
|
|||||||
error_t (*get_backlight)(struct Device* device, struct Device** backlight_device);
|
error_t (*get_backlight)(struct Device* device, struct Device** backlight_device);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Optional: reports whether the keyboard is physically present right now. Only
|
* @deprecated Use Driver::probe + device_hotplug_register() instead (see DEVICE_FLAG_HOTPLUG)
|
||||||
* meaningful for hot-pluggable/detachable keyboards (e.g. a removable accessory) whose
|
* The kernel then starts/stops the device itself instead of leaving it started forever.
|
||||||
* kernel device is constructed and started once at boot regardless of physical attachment -
|
* When DEVICE_FLAG_HOTPLUG is used, use device_is_ready() only for checking if a keyboard is usable.
|
||||||
* leave NULL for a keyboard that's always physically present whenever its device is active
|
|
||||||
* (the common case; callers must treat NULL the same as "always present").
|
|
||||||
* @param[in] device the keyboard device
|
|
||||||
* @return true if physically attached/present
|
|
||||||
*/
|
*/
|
||||||
bool (*is_present)(struct Device* device);
|
bool (*is_present)(struct Device* device);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reads the next pending key event using the specified keyboard device.
|
* @brief Reads the next pending key event using the specified keyboard device. Also fans the
|
||||||
|
* result out to every keyboard_subscribe()'d subscriber, same as keyboard_emit_key().
|
||||||
|
* @retval ERROR_NONE @a data filled - key == 0 if nothing is pending
|
||||||
|
* @retval ERROR_INVALID_STATE @a device isn't currently started (e.g. hotplug-absent)
|
||||||
|
* @retval ERROR_NOT_SUPPORTED the driver has no KeyboardApi::read_key (it pushes events via
|
||||||
|
* keyboard_emit_key() instead)
|
||||||
*/
|
*/
|
||||||
error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
|
error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Push a key event from @a device to every current subscriber (see keyboard_subscribe()).
|
||||||
|
* For a driver whose hardware delivers key events asynchronously (e.g. from a dedicated
|
||||||
|
* interrupt-handling task) rather than on demand - KeyboardApi::read_key may be left NULL when
|
||||||
|
* the driver calls this instead.
|
||||||
|
* @warning Not ISR-safe (takes a mutex); call from a task, not directly from interrupt context.
|
||||||
|
* @param[in] device the keyboard device the event originated from
|
||||||
|
* @param[in] data the key event
|
||||||
|
*/
|
||||||
|
void keyboard_emit_key(struct Device* device, struct KeyboardKeyData data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the backlight if the keyboard has one.
|
* @brief Returns the backlight if the keyboard has one.
|
||||||
* @warning Returns a referenced device. Must call device_put() afterwards.
|
* @warning Returns a referenced device. Must call device_put() afterwards.
|
||||||
@ -141,13 +173,33 @@ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
|
|||||||
error_t keyboard_get_backlight(struct Device* device, struct Device** backlight_device);
|
error_t keyboard_get_backlight(struct Device* device, struct Device** backlight_device);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the keyboard device is physically present right now. True when the driver
|
* @deprecated See KeyboardApi::is_present.
|
||||||
* doesn't implement KeyboardApi::is_present (i.e. it's always physically present whenever its
|
|
||||||
* device is active) - see that field's doc comment.
|
|
||||||
* @param[in] device the keyboard device
|
* @param[in] device the keyboard device
|
||||||
*/
|
*/
|
||||||
bool keyboard_is_present(struct Device* device);
|
bool keyboard_is_present(struct Device* device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register @a sub for async key events from @a device, derived from `read_key`.
|
||||||
|
* @param[in,out] sub subscription to register; owns the storage, must stay alive (and stationary)
|
||||||
|
* until unsubscribed
|
||||||
|
* @retval ERROR_NONE on success
|
||||||
|
*/
|
||||||
|
error_t keyboard_subscribe(struct Device* device, struct KeyboardEventSubscription* sub);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove a subscription previously registered with keyboard_subscribe().
|
||||||
|
* @retval ERROR_NONE on success
|
||||||
|
*/
|
||||||
|
error_t keyboard_unsubscribe(struct Device* device, struct KeyboardEventSubscription* sub);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Non-blocking: pop the next event for @a sub if one is already queued.
|
||||||
|
* @warning Never blocks.
|
||||||
|
* @retval ERROR_NONE @a out_data was filled
|
||||||
|
* @retval ERROR_TIMEOUT nothing queued right now
|
||||||
|
*/
|
||||||
|
error_t keyboard_poll(struct Device* device, struct KeyboardEventSubscription* sub, struct KeyboardKeyData* out_data);
|
||||||
|
|
||||||
extern const struct DeviceType KEYBOARD_TYPE;
|
extern const struct DeviceType KEYBOARD_TYPE;
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
#include <tactility/check.h>
|
#include <tactility/check.h>
|
||||||
#include <tactility/concurrent/recursive_mutex.h>
|
#include <tactility/concurrent/recursive_mutex.h>
|
||||||
|
#include <tactility/concurrent/timer.h>
|
||||||
|
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@ -43,6 +44,11 @@ struct DeviceInternal {
|
|||||||
} state;
|
} state;
|
||||||
/** Attached child devices */
|
/** Attached child devices */
|
||||||
std::vector<Device*> children {};
|
std::vector<Device*> children {};
|
||||||
|
// device_hotplug_poll_once() debounce state.
|
||||||
|
struct {
|
||||||
|
uint8_t consecutive = 0; // probe() results differing from `present` so far
|
||||||
|
bool present = false;
|
||||||
|
} hotplug;
|
||||||
// Outstanding device_get() holders. Guarded by `mutex`. Independent of state.started -
|
// Outstanding device_get() holders. Guarded by `mutex`. Independent of state.started -
|
||||||
// device_get()/device_put() bracket construct/destruct, not start/stop, so a ref can be held
|
// device_get()/device_put() bracket construct/destruct, not start/stop, so a ref can be held
|
||||||
// across a device_stop(). device_destruct() refuses to run while this is > 0.
|
// across a device_stop(). device_destruct() refuses to run while this is > 0.
|
||||||
@ -225,7 +231,7 @@ error_t device_start(Device* device) {
|
|||||||
unlock_internal(internal);
|
unlock_internal(internal);
|
||||||
|
|
||||||
if (bind_error != ERROR_NONE) {
|
if (bind_error != ERROR_NONE) {
|
||||||
return ERROR_RESOURCE;
|
return bind_error == ERROR_NOT_FOUND ? ERROR_NOT_FOUND : ERROR_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
device_listener_notify(device, DEVICE_EVENT_STARTED);
|
device_listener_notify(device, DEVICE_EVENT_STARTED);
|
||||||
@ -559,4 +565,131 @@ error_t device_get_first_by_compatible(const char* compatible, Device** out_devi
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct HotplugAction {
|
||||||
|
Device* device;
|
||||||
|
bool start; // vs. stop
|
||||||
|
bool mandatory; // probe == nullptr; a start failure here is a real error
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guards hotplug_registered_devices only - never held during probe()/device_start()/device_stop().
|
||||||
|
struct HotplugRegistryMutex {
|
||||||
|
Mutex handle {};
|
||||||
|
HotplugRegistryMutex() { mutex_construct(&handle); }
|
||||||
|
~HotplugRegistryMutex() { mutex_destruct(&handle); }
|
||||||
|
};
|
||||||
|
|
||||||
|
static HotplugRegistryMutex hotplug_registry_mutex;
|
||||||
|
static std::vector<Device*> hotplug_registered_devices;
|
||||||
|
|
||||||
|
static Timer* hotplug_timer = nullptr;
|
||||||
|
|
||||||
|
static void hotplug_timer_callback(void*) {
|
||||||
|
device_hotplug_poll_once();
|
||||||
|
}
|
||||||
|
|
||||||
|
void device_hotplug_register(Device* device) {
|
||||||
|
mutex_lock(&hotplug_registry_mutex.handle);
|
||||||
|
if (std::ranges::find(hotplug_registered_devices, device) == hotplug_registered_devices.end()) {
|
||||||
|
if (device->internal->driver != nullptr && device->internal->driver->probe != nullptr) {
|
||||||
|
device->flags |= DEVICE_FLAG_HOTPLUG;
|
||||||
|
}
|
||||||
|
hotplug_registered_devices.push_back(device);
|
||||||
|
}
|
||||||
|
mutex_unlock(&hotplug_registry_mutex.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void device_hotplug_unregister(Device* device) {
|
||||||
|
mutex_lock(&hotplug_registry_mutex.handle);
|
||||||
|
const auto iterator = std::ranges::find(hotplug_registered_devices, device);
|
||||||
|
if (iterator != hotplug_registered_devices.end()) {
|
||||||
|
hotplug_registered_devices.erase(iterator);
|
||||||
|
device->flags &= ~DEVICE_FLAG_HOTPLUG;
|
||||||
|
}
|
||||||
|
mutex_unlock(&hotplug_registry_mutex.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t device_hotplug_poll_once(void) {
|
||||||
|
// Pin each device (device_get()) so a concurrent unregister+destruct can't free it mid-loop -
|
||||||
|
// device_destruct() refuses to run while a reference is outstanding.
|
||||||
|
std::vector<Device*> devices;
|
||||||
|
mutex_lock(&hotplug_registry_mutex.handle);
|
||||||
|
for (Device* device : hotplug_registered_devices) {
|
||||||
|
if (device_get(device) == ERROR_NONE) {
|
||||||
|
devices.push_back(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&hotplug_registry_mutex.handle);
|
||||||
|
|
||||||
|
// Collect actions first: start_device()/stop_device() may add/remove child devices
|
||||||
|
// (ledger_lock), so don't call them mid-iteration. Device state is read/written under its own
|
||||||
|
// mutex, released before calling probe() - never run caller code under a lock.
|
||||||
|
std::vector<HotplugAction> actions;
|
||||||
|
for (Device* device : devices) {
|
||||||
|
auto* internal = device->internal;
|
||||||
|
|
||||||
|
lock_internal(internal);
|
||||||
|
auto* driver = internal->driver;
|
||||||
|
bool started = internal->state.started;
|
||||||
|
unlock_internal(internal);
|
||||||
|
|
||||||
|
if (driver == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driver->probe == nullptr) {
|
||||||
|
if (!started) {
|
||||||
|
actions.push_back({ device, true, true });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool present = driver->probe(device) == ERROR_NONE;
|
||||||
|
|
||||||
|
lock_internal(internal);
|
||||||
|
bool act = false;
|
||||||
|
if (present == internal->hotplug.present) {
|
||||||
|
internal->hotplug.consecutive = 0;
|
||||||
|
} else if (++internal->hotplug.consecutive >= DEVICE_HOTPLUG_DEBOUNCE_COUNT) {
|
||||||
|
// hotplug.present isn't committed yet - only once the action below actually
|
||||||
|
// succeeds. Otherwise a failed device_start()/device_stop() would leave it matching
|
||||||
|
// the next stable probe(), and the poller would never retry.
|
||||||
|
internal->hotplug.consecutive = 0;
|
||||||
|
act = true;
|
||||||
|
}
|
||||||
|
unlock_internal(internal);
|
||||||
|
|
||||||
|
if (act) {
|
||||||
|
actions.push_back({ device, present, false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t first_mandatory_error = ERROR_NONE;
|
||||||
|
for (const auto& action : actions) {
|
||||||
|
error_t error = action.start ? device_start(action.device) : device_stop(action.device);
|
||||||
|
if (error == ERROR_NONE && !action.mandatory) {
|
||||||
|
auto* internal = action.device->internal;
|
||||||
|
lock_internal(internal);
|
||||||
|
internal->hotplug.present = action.start;
|
||||||
|
unlock_internal(internal);
|
||||||
|
}
|
||||||
|
if (action.mandatory && action.start && error != ERROR_NONE && first_mandatory_error == ERROR_NONE) {
|
||||||
|
first_mandatory_error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Device* device : devices) {
|
||||||
|
device_put(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
return first_mandatory_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void device_hotplug_start_polling(void) {
|
||||||
|
if (hotplug_timer != nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hotplug_timer = timer_alloc(TIMER_TYPE_PERIODIC, pdMS_TO_TICKS(500), hotplug_timer_callback, nullptr);
|
||||||
|
timer_start(hotplug_timer);
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@ -129,6 +129,11 @@ error_t driver_bind(Driver* driver, Device* device) {
|
|||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (driver->probe != nullptr && driver->probe(device) != ERROR_NONE) {
|
||||||
|
error = ERROR_NOT_FOUND;
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
if (driver->start_device != nullptr) {
|
if (driver->start_device != nullptr) {
|
||||||
error = driver->start_device(device);
|
error = driver->start_device(device);
|
||||||
if (error != ERROR_NONE) {
|
if (error != ERROR_NONE) {
|
||||||
|
|||||||
@ -3,21 +3,86 @@
|
|||||||
#include <tactility/drivers/keyboard.h>
|
#include <tactility/drivers/keyboard.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/mutex.h>
|
||||||
|
|
||||||
#define KEYBOARD_DRIVER_API(driver) ((struct KeyboardApi*)driver->api)
|
#define KEYBOARD_DRIVER_API(driver) ((struct KeyboardApi*)driver->api)
|
||||||
|
|
||||||
|
// Subscriptions, filtered by `device` on fan-out.
|
||||||
|
static KeyboardEventSubscription* subscriptions = nullptr;
|
||||||
|
|
||||||
|
struct KeyboardEventMutex {
|
||||||
|
Mutex handle {};
|
||||||
|
KeyboardEventMutex() { mutex_construct(&handle); }
|
||||||
|
~KeyboardEventMutex() { mutex_destruct(&handle); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// One coarse mutex is fine: fan-out is just a struct copy, never caller code.
|
||||||
|
static KeyboardEventMutex subscriptions_mutex;
|
||||||
|
|
||||||
|
// Caller must hold subscriptions_mutex.
|
||||||
|
static void fan_out_to_subscribers(Device* device, const KeyboardKeyData& data) {
|
||||||
|
for (KeyboardEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||||
|
if (sub->internal.device != device) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sub->internal.count < KEYBOARD_EVENT_QUEUE_CAPACITY) {
|
||||||
|
uint8_t tail = (sub->internal.head + sub->internal.count) % KEYBOARD_EVENT_QUEUE_CAPACITY;
|
||||||
|
sub->internal.queue[tail] = data;
|
||||||
|
sub->internal.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool try_pop(KeyboardEventSubscription* sub, KeyboardKeyData* out_data) {
|
||||||
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
|
bool has_event = sub->internal.count > 0;
|
||||||
|
if (has_event) {
|
||||||
|
*out_data = sub->internal.queue[sub->internal.head];
|
||||||
|
sub->internal.head = (sub->internal.head + 1) % KEYBOARD_EVENT_QUEUE_CAPACITY;
|
||||||
|
sub->internal.count--;
|
||||||
|
}
|
||||||
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
return has_event;
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
error_t keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
error_t keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
||||||
|
// Under the hotplug poller a keyboard device can be constructed but not started (probe
|
||||||
|
// hasn't confirmed presence, or it's since detached) - a sync caller like LVGL's indev still
|
||||||
|
// polls it every tick regardless, so bail out rather than touching the driver's (possibly
|
||||||
|
// unallocated) state.
|
||||||
|
if (!device_is_ready(device)) {
|
||||||
|
*data = {};
|
||||||
|
return ERROR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
const auto* driver = device_get_driver(device);
|
const auto* driver = device_get_driver(device);
|
||||||
|
|
||||||
// Default the modifier/HID fields here rather than in each driver: only drivers whose hardware
|
if (KEYBOARD_DRIVER_API(driver)->read_key == nullptr) {
|
||||||
// can report them set them, and the rest would otherwise leave whatever the caller's stack held.
|
return ERROR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaulted here, not per-driver: only drivers whose hardware reports them set them.
|
||||||
data->ctrl = false;
|
data->ctrl = false;
|
||||||
data->alt = false;
|
data->alt = false;
|
||||||
data->hid_keycode = 0;
|
data->hid_keycode = 0;
|
||||||
data->hid_modifier = 0;
|
data->hid_modifier = 0;
|
||||||
|
|
||||||
return KEYBOARD_DRIVER_API(driver)->read_key(device, data);
|
error_t result = KEYBOARD_DRIVER_API(driver)->read_key(device, data);
|
||||||
|
if (result == ERROR_NONE && data->key != 0) {
|
||||||
|
// A sync caller (e.g. LVGL) isn't the only consumer of this key.
|
||||||
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
|
fan_out_to_subscribers(device, *data);
|
||||||
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void keyboard_emit_key(Device* device, KeyboardKeyData data) {
|
||||||
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
|
fan_out_to_subscribers(device, data);
|
||||||
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t keyboard_get_backlight(Device* device, Device** backlight_device) {
|
error_t keyboard_get_backlight(Device* device, Device** backlight_device) {
|
||||||
@ -40,8 +105,59 @@ bool keyboard_is_present(Device* device) {
|
|||||||
return KEYBOARD_DRIVER_API(driver)->is_present(device);
|
return KEYBOARD_DRIVER_API(driver)->is_present(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_t keyboard_subscribe(Device* device, KeyboardEventSubscription* sub) {
|
||||||
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
|
|
||||||
|
// Avoid cyclic subscription list that would loop forever
|
||||||
|
for (auto* current = subscriptions; current != nullptr; current = current->internal.next) {
|
||||||
|
if (current == sub) {
|
||||||
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
return ERROR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub->internal.device = device;
|
||||||
|
sub->internal.head = 0;
|
||||||
|
sub->internal.count = 0;
|
||||||
|
sub->internal.next = subscriptions;
|
||||||
|
subscriptions = sub;
|
||||||
|
|
||||||
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t keyboard_unsubscribe(Device*, KeyboardEventSubscription* sub) {
|
||||||
|
error_t result = ERROR_NOT_FOUND;
|
||||||
|
|
||||||
|
mutex_lock(&subscriptions_mutex.handle);
|
||||||
|
for (KeyboardEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||||
|
if (*link == sub) {
|
||||||
|
*link = sub->internal.next;
|
||||||
|
result = ERROR_NONE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&subscriptions_mutex.handle);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t keyboard_poll(Device* device, KeyboardEventSubscription* sub, KeyboardKeyData* out_data) {
|
||||||
|
if (try_pop(sub, out_data)) {
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyboard_read_key() fans the result out to every subscriber, including `sub`.
|
||||||
|
KeyboardKeyData data;
|
||||||
|
if (keyboard_read_key(device, &data) != ERROR_NONE || data.key == 0) {
|
||||||
|
return ERROR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return try_pop(sub, out_data) ? ERROR_NONE : ERROR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
const DeviceType KEYBOARD_TYPE {
|
const DeviceType KEYBOARD_TYPE {
|
||||||
.name = "keyboard"
|
.name = "keyboard",
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,13 +65,16 @@ error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[])
|
|||||||
dts_module++;
|
dts_module++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Starting is deferred to device_hotplug_poll_once() below so probe() can run first.
|
||||||
|
// DISABLED devices are never registered, so the poller never starts them.
|
||||||
const DtsDevice* dts_device = dts_devices;
|
const DtsDevice* dts_device = dts_devices;
|
||||||
while (dts_device->device != nullptr) {
|
while (dts_device->device != nullptr) {
|
||||||
if (dts_device->status == DTS_DEVICE_STATUS_OKAY) {
|
if (dts_device->status == DTS_DEVICE_STATUS_OKAY) {
|
||||||
if (device_construct_add_start(dts_device->device, dts_device->compatible) != ERROR_NONE) {
|
if (device_construct_add(dts_device->device, dts_device->compatible) != ERROR_NONE) {
|
||||||
LOG_E(TAG, "kernel_init failed to construct+add+start device: %s (%s)", dts_device->device->name, dts_device->compatible);
|
LOG_E(TAG, "kernel_init failed to construct+add device: %s (%s)", dts_device->device->name, dts_device->compatible);
|
||||||
return ERROR_RESOURCE;
|
return ERROR_RESOURCE;
|
||||||
}
|
}
|
||||||
|
device_hotplug_register(dts_device->device);
|
||||||
} else if (dts_device->status == DTS_DEVICE_STATUS_DISABLED) {
|
} else if (dts_device->status == DTS_DEVICE_STATUS_DISABLED) {
|
||||||
if (device_construct_add(dts_device->device, dts_device->compatible) != ERROR_NONE) {
|
if (device_construct_add(dts_device->device, dts_device->compatible) != ERROR_NONE) {
|
||||||
LOG_E(TAG, "kernel_init failed to construct+add device: %s (%s)", dts_device->device->name, dts_device->compatible);
|
LOG_E(TAG, "kernel_init failed to construct+add device: %s (%s)", dts_device->device->name, dts_device->compatible);
|
||||||
@ -83,6 +86,13 @@ error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[])
|
|||||||
dts_device++;
|
dts_device++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A mandatory (probe == NULL) device's start failure aborts boot, as before.
|
||||||
|
if (device_hotplug_poll_once() != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "kernel_init failed to start a mandatory device");
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
device_hotplug_start_polling();
|
||||||
|
|
||||||
LOG_I(TAG, "init done");
|
LOG_I(TAG, "init done");
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|||||||
129
TactilityKernel/tests/source/device_hotplug_test.cpp
Normal file
129
TactilityKernel/tests/source/device_hotplug_test.cpp
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
#include "doctest.h"
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/driver.h>
|
||||||
|
#include <tactility/module.h>
|
||||||
|
|
||||||
|
static Module hotplug_test_module = {
|
||||||
|
.name = "hotplug_test_module",
|
||||||
|
.start = nullptr,
|
||||||
|
.stop = nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool probe_present = false;
|
||||||
|
static int probe_driver_start_called = 0;
|
||||||
|
static int probe_driver_stop_called = 0;
|
||||||
|
|
||||||
|
static error_t probe_driver_probe(Device*) { return probe_present ? ERROR_NONE : ERROR_NOT_FOUND; }
|
||||||
|
static error_t probe_driver_start(Device*) { probe_driver_start_called++; return ERROR_NONE; }
|
||||||
|
static error_t probe_driver_stop(Device*) { probe_driver_stop_called++; return ERROR_NONE; }
|
||||||
|
|
||||||
|
static Driver probe_driver = {
|
||||||
|
.name = "hotplug_test_probe_driver",
|
||||||
|
.compatible = (const char*[]) { "hotplug_test,probe", nullptr },
|
||||||
|
.start_device = probe_driver_start,
|
||||||
|
.stop_device = probe_driver_stop,
|
||||||
|
.probe = probe_driver_probe,
|
||||||
|
.api = nullptr,
|
||||||
|
.device_type = nullptr,
|
||||||
|
.owner = &hotplug_test_module,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("device_hotplug_poll_once starts/stops a probe-capable device after debounce") {
|
||||||
|
probe_present = false;
|
||||||
|
probe_driver_start_called = 0;
|
||||||
|
probe_driver_stop_called = 0;
|
||||||
|
|
||||||
|
static Device device {
|
||||||
|
.name = "hotplug_probe_device",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&probe_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&device, "hotplug_test,probe"), ERROR_NONE);
|
||||||
|
device_hotplug_register(&device);
|
||||||
|
|
||||||
|
// Not present: repeated polling never starts it.
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK_EQ(probe_driver_start_called, 0);
|
||||||
|
CHECK_FALSE(device_is_ready(&device));
|
||||||
|
|
||||||
|
// Present: needs DEVICE_HOTPLUG_DEBOUNCE_COUNT consecutive polls before it actually starts.
|
||||||
|
probe_present = true;
|
||||||
|
for (int i = 0; i < DEVICE_HOTPLUG_DEBOUNCE_COUNT - 1; i++) {
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK_FALSE(device_is_ready(&device));
|
||||||
|
}
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK(device_is_ready(&device));
|
||||||
|
CHECK_EQ(probe_driver_start_called, 1);
|
||||||
|
|
||||||
|
// Already started: further polls while still present don't restart it.
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK_EQ(probe_driver_start_called, 1);
|
||||||
|
|
||||||
|
// Absent again: needs the same debounce before it stops.
|
||||||
|
probe_present = false;
|
||||||
|
for (int i = 0; i < DEVICE_HOTPLUG_DEBOUNCE_COUNT - 1; i++) {
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK(device_is_ready(&device));
|
||||||
|
}
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK_FALSE(device_is_ready(&device));
|
||||||
|
CHECK_EQ(probe_driver_stop_called, 1);
|
||||||
|
|
||||||
|
// Only started/stopped, never removed/destructed.
|
||||||
|
CHECK(device_is_added(&device));
|
||||||
|
|
||||||
|
device_hotplug_unregister(&device);
|
||||||
|
CHECK_EQ(device_remove(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&probe_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int mandatory_driver_start_called = 0;
|
||||||
|
|
||||||
|
static error_t mandatory_driver_start(Device*) { mandatory_driver_start_called++; return ERROR_NONE; }
|
||||||
|
static error_t mandatory_driver_stop(Device*) { return ERROR_NONE; }
|
||||||
|
|
||||||
|
static Driver mandatory_driver = {
|
||||||
|
.name = "hotplug_test_mandatory_driver",
|
||||||
|
.compatible = (const char*[]) { "hotplug_test,mandatory", nullptr },
|
||||||
|
.start_device = mandatory_driver_start,
|
||||||
|
.stop_device = mandatory_driver_stop,
|
||||||
|
.probe = nullptr,
|
||||||
|
.api = nullptr,
|
||||||
|
.device_type = nullptr,
|
||||||
|
.owner = &hotplug_test_module,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("device_hotplug_poll_once starts a mandatory (probe == NULL) device unconditionally on the first call") {
|
||||||
|
mandatory_driver_start_called = 0;
|
||||||
|
|
||||||
|
static Device device {
|
||||||
|
.name = "hotplug_mandatory_device",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&mandatory_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&device, "hotplug_test,mandatory"), ERROR_NONE);
|
||||||
|
device_hotplug_register(&device);
|
||||||
|
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK(device_is_ready(&device));
|
||||||
|
CHECK_EQ(mandatory_driver_start_called, 1);
|
||||||
|
|
||||||
|
// Already started: further polls don't restart it.
|
||||||
|
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
|
||||||
|
CHECK_EQ(mandatory_driver_start_called, 1);
|
||||||
|
|
||||||
|
device_hotplug_unregister(&device);
|
||||||
|
CHECK_EQ(device_stop(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&mandatory_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
225
TactilityKernel/tests/source/keyboard_test.cpp
Normal file
225
TactilityKernel/tests/source/keyboard_test.cpp
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
#include "doctest.h"
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/driver.h>
|
||||||
|
#include <tactility/drivers/keyboard.h>
|
||||||
|
#include <tactility/module.h>
|
||||||
|
|
||||||
|
static Module keyboard_test_module = {
|
||||||
|
.name = "keyboard_test_module",
|
||||||
|
.start = nullptr,
|
||||||
|
.stop = nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
static int read_key_call_count = 0;
|
||||||
|
|
||||||
|
// First call reports a key press, every call after that reports nothing pending (key == 0),
|
||||||
|
// matching the convention every real driver uses (see e.g. sdl_keyboard_read_key).
|
||||||
|
static error_t fake_read_key(Device*, KeyboardKeyData* data) {
|
||||||
|
read_key_call_count++;
|
||||||
|
if (read_key_call_count == 1) {
|
||||||
|
data->key = 'a';
|
||||||
|
data->pressed = true;
|
||||||
|
data->continue_reading = false;
|
||||||
|
} else {
|
||||||
|
data->key = 0;
|
||||||
|
data->pressed = false;
|
||||||
|
data->continue_reading = false;
|
||||||
|
}
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const KeyboardApi fake_keyboard_api = {
|
||||||
|
.read_key = fake_read_key,
|
||||||
|
.get_backlight = nullptr,
|
||||||
|
.is_present = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
static Driver fake_keyboard_driver = {
|
||||||
|
.name = "fake_keyboard_driver",
|
||||||
|
.compatible = (const char*[]) { "keyboard_test,fake", nullptr },
|
||||||
|
.start_device = nullptr,
|
||||||
|
.stop_device = nullptr,
|
||||||
|
.api = &fake_keyboard_api,
|
||||||
|
.device_type = &KEYBOARD_TYPE,
|
||||||
|
.owner = &keyboard_test_module,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
// No read_key: this driver only ever pushes events via keyboard_emit_key().
|
||||||
|
static const KeyboardApi fake_emit_only_keyboard_api = {
|
||||||
|
.read_key = nullptr,
|
||||||
|
.get_backlight = nullptr,
|
||||||
|
.is_present = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
static Driver fake_emit_only_keyboard_driver = {
|
||||||
|
.name = "fake_emit_only_keyboard_driver",
|
||||||
|
.compatible = (const char*[]) { "keyboard_test,fake_emit_only", nullptr },
|
||||||
|
.start_device = nullptr,
|
||||||
|
.stop_device = nullptr,
|
||||||
|
.api = &fake_emit_only_keyboard_api,
|
||||||
|
.device_type = &KEYBOARD_TYPE,
|
||||||
|
.owner = &keyboard_test_module,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("keyboard_read_key returns ERROR_INVALID_STATE, without touching the driver, on a device that isn't started") {
|
||||||
|
read_key_call_count = 0;
|
||||||
|
|
||||||
|
static Device fake_device {
|
||||||
|
.name = "fake_keyboard_device_not_started",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE);
|
||||||
|
// Deliberately not started - a hotplug-managed keyboard can be constructed+added long before
|
||||||
|
// (or after) its driver is actually running, but LVGL's indev still polls it unconditionally.
|
||||||
|
|
||||||
|
KeyboardKeyData out { .key = 'x' };
|
||||||
|
CHECK_EQ(keyboard_read_key(&fake_device, &out), ERROR_INVALID_STATE);
|
||||||
|
CHECK_EQ(out.key, 0);
|
||||||
|
CHECK_EQ(read_key_call_count, 0);
|
||||||
|
|
||||||
|
CHECK_EQ(device_remove(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("keyboard_poll's default implementation fans out a read_key result to other subscribers") {
|
||||||
|
read_key_call_count = 0;
|
||||||
|
|
||||||
|
static Device fake_device {
|
||||||
|
.name = "fake_keyboard_device",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_start(&fake_device), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardEventSubscription sub_a {};
|
||||||
|
KeyboardEventSubscription sub_b {};
|
||||||
|
CHECK_EQ(keyboard_subscribe(&fake_device, &sub_a), ERROR_NONE);
|
||||||
|
CHECK_EQ(keyboard_subscribe(&fake_device, &sub_b), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardKeyData out {};
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub_a, &out), ERROR_NONE);
|
||||||
|
CHECK_EQ(out.key, 'a');
|
||||||
|
CHECK_EQ(read_key_call_count, 1);
|
||||||
|
|
||||||
|
// sub_b receives the same event without a second hardware read - it was fanned out by sub_a's poll.
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_NONE);
|
||||||
|
CHECK_EQ(out.key, 'a');
|
||||||
|
CHECK_EQ(read_key_call_count, 1);
|
||||||
|
|
||||||
|
// sub_b's queue is now empty; polling again falls through to a real (empty) hardware read.
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_TIMEOUT);
|
||||||
|
CHECK_EQ(read_key_call_count, 2);
|
||||||
|
|
||||||
|
CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_a), ERROR_NONE);
|
||||||
|
CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_b), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_stop(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("keyboard_read_key fans out to subscribers even when called directly") {
|
||||||
|
read_key_call_count = 0;
|
||||||
|
|
||||||
|
static Device fake_device {
|
||||||
|
.name = "fake_keyboard_device_read_key_fanout",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_start(&fake_device), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardEventSubscription sub {};
|
||||||
|
CHECK_EQ(keyboard_subscribe(&fake_device, &sub), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardKeyData out {};
|
||||||
|
CHECK_EQ(keyboard_read_key(&fake_device, &out), ERROR_NONE);
|
||||||
|
CHECK_EQ(out.key, 'a');
|
||||||
|
|
||||||
|
// sub never called keyboard_poll() itself - it received this via keyboard_read_key()'s fan-out.
|
||||||
|
KeyboardKeyData polled {};
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub, &polled), ERROR_NONE);
|
||||||
|
CHECK_EQ(polled.key, 'a');
|
||||||
|
CHECK_EQ(read_key_call_count, 1);
|
||||||
|
|
||||||
|
CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_stop(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("keyboard_emit_key delivers to subscribers of a driver with no read_key") {
|
||||||
|
static Device fake_device {
|
||||||
|
.name = "fake_emit_only_keyboard_device",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&fake_emit_only_keyboard_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake_emit_only"), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_start(&fake_device), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardEventSubscription sub {};
|
||||||
|
CHECK_EQ(keyboard_subscribe(&fake_device, &sub), ERROR_NONE);
|
||||||
|
|
||||||
|
// Nothing pushed yet, and there's no read_key to fall back on.
|
||||||
|
KeyboardKeyData out {};
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub, &out), ERROR_TIMEOUT);
|
||||||
|
|
||||||
|
KeyboardKeyData emitted { .key = 'z', .pressed = true, .continue_reading = false };
|
||||||
|
keyboard_emit_key(&fake_device, emitted);
|
||||||
|
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub, &out), ERROR_NONE);
|
||||||
|
CHECK_EQ(out.key, 'z');
|
||||||
|
|
||||||
|
CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_stop(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&fake_emit_only_keyboard_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("keyboard_unsubscribe stops a subscription from receiving further fanned-out events") {
|
||||||
|
read_key_call_count = 0;
|
||||||
|
|
||||||
|
static Device fake_device {
|
||||||
|
.name = "fake_keyboard_device_2",
|
||||||
|
.config = nullptr,
|
||||||
|
.parent = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_start(&fake_device), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardEventSubscription sub_a {};
|
||||||
|
KeyboardEventSubscription sub_b {};
|
||||||
|
CHECK_EQ(keyboard_subscribe(&fake_device, &sub_a), ERROR_NONE);
|
||||||
|
CHECK_EQ(keyboard_subscribe(&fake_device, &sub_b), ERROR_NONE);
|
||||||
|
CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_b), ERROR_NONE);
|
||||||
|
|
||||||
|
KeyboardKeyData out {};
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub_a, &out), ERROR_NONE);
|
||||||
|
CHECK_EQ(out.key, 'a');
|
||||||
|
|
||||||
|
// sub_b was unsubscribed before the poll, so nothing was fanned out to it.
|
||||||
|
CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_TIMEOUT);
|
||||||
|
|
||||||
|
CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_a), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_stop(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&fake_device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user