Compare commits

..

2 Commits

Author SHA1 Message Date
Ken Van Hoeylandt
d2c69ee7e8
Move service API from TactilityKernel to service-module (#604)
Extracted the service API from TactilityKernel and put it in a new module at `Modules/service-module`
2026-08-01 20:51:43 +02:00
Shadowtrance
85fe1a319a
Tab5 ST7121 variant + fixes (#605)
Added the newest variant to the tab5, St7121.
Fixed variant detection reliability
Fixed tab5 camera WHO_AM_I failing sometimes
Fixed tab5 keyboard live rotation on boot and after stopping/starting lvgl
2026-08-01 20:50:55 +02:00
63 changed files with 2075 additions and 269 deletions

8
.gitignore vendored
View File

@ -1,10 +1,8 @@
.idea/
.DS_Store
build/
buildsim/
build-*/
cmake-*/
build*/
cmake*/
CMakeCache.txt
*.cbp
CMakeFiles
@ -26,4 +24,4 @@ sdkconfig.board.*.dev
.tactility/
.caveman.json
.ai/mcp
.ai/mcp

View File

@ -200,6 +200,7 @@ def main():
add_module(target_path, "lvgl-module")
add_module(target_path, "crypt-module")
add_module(target_path, "gps-module")
add_module(target_path, "service-module")
# Drivers - only ones actually built for this target (chip-restricted drivers like
# sc2356-module won't have a .a outside ESP32-P4)

View File

@ -98,6 +98,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Modules/lvgl-module)
add_subdirectory(Modules/crypt-module)
add_subdirectory(Modules/gps-module)
add_subdirectory(Modules/service-module)
add_subdirectory(Drivers/gps-generic-module)
add_subdirectory(Drivers/gps-meshtastic-module)

View File

@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES TactilityKernel lvgl-module ina226-module ili9881c-module st7123-module gt911-module
REQUIRES TactilityKernel lvgl-module ina226-module ili9881c-module st7121-module st7123-module gt911-module
)

View File

@ -21,6 +21,34 @@ void tab5_set_variant(Tab5Variant variant) {
detected_variant = variant;
}
// The ST7123 (V2) and ST7121 (V3) touch controllers share the same fixed I2C address, so presence
// alone doesn't distinguish them. Both expose a firmware-version byte at register 0x0000 (a 16-bit
// register address, per ESP_LCD_TOUCH_IO_I2C_ST7123_CONFIG's lcd_cmd_bits=16 - see the M5Tab5
// UserDemo's bsp_detect_display_type()): fw_version 1 means ST7121/V3, fw_version 3 means
// ST7123/V2. Returns false (leaving *out_variant untouched) if the register read fails or reports
// an unrecognized value, so the caller's outer attempt loop can retry rather than the touch IC's
// transient not-finished-booting state permanently misdetecting V3 hardware as V2.
static bool probe_st7123_or_st7121_variant(Device* i2c0, TickType_t timeout, Tab5Variant* out_variant) {
const uint8_t reg_addr[2] = {0x00, 0x00};
uint8_t fw_version = 0;
if (i2c_controller_write_read(i2c0, ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS, reg_addr, sizeof(reg_addr), &fw_version, 1, timeout) != ERROR_NONE) {
LOG_W(TAG, "display_detect: failed to read touch FW version, retrying");
return false;
}
if (fw_version == 1) {
LOG_I(TAG, "display_detect: detected ST7121 touch (FW version 1) — using variant V3");
*out_variant = Tab5Variant::V3;
return true;
}
if (fw_version == 3) {
LOG_I(TAG, "display_detect: detected ST7123 touch (FW version 3) — using variant V2");
*out_variant = Tab5Variant::V2;
return true;
}
LOG_W(TAG, "display_detect: touch at ST7123 address reported unknown FW version %u, retrying", fw_version);
return false;
}
Tab5Variant tab5_probe_variant(Device* i2c0) {
// Allow time for the touch IC to fully boot after the reset pulse above: 100ms is enough for
// I2C ACK (probe) but cold power-on needs ~300ms before register reads succeed reliably.
@ -36,8 +64,12 @@ Tab5Variant tab5_probe_variant(Device* i2c0) {
}
if (i2c_controller_has_device_at_address(i2c0, ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS, PROBE_TIMEOUT) == ERROR_NONE) {
LOG_I(TAG, "display_detect: detected ST7123 touch — using variant V2");
return Tab5Variant::V2;
Tab5Variant variant;
if (probe_st7123_or_st7121_variant(i2c0, PROBE_TIMEOUT, &variant)) {
return variant;
}
// FW-version read failed/unrecognized - fall through to the retry delay below instead
// of giving up immediately, same as an address-probe miss.
}
vTaskDelay(pdMS_TO_TICKS(100));

View File

@ -6,6 +6,7 @@ enum class Tab5Variant {
Unknown,
V1, // Older variant: ILI9881C display + GT911 touch (see devices_v1.cpp)
V2, // Newer variant (default): ST7123 display + in-cell touch (see devices_v2.cpp)
V3, // Newest variant: ST7121 display + in-cell touch (see devices_v3.cpp)
};
// Populated once the device_listener callback in display_detect.cpp has detected which

View File

@ -1,6 +1,7 @@
#include "devices_v2.h"
#include "devices_common.h"
#include "devices_v2_v3_touch.h"
#include "st7123_init_data.h"
#include <tactility/device.h>
@ -8,7 +9,6 @@
#include <tactility/log.h>
#include <drivers/st7123.h>
#include <drivers/st7123_touch.h>
#include <iterator>
#include <vector>
@ -19,43 +19,6 @@ static std::vector<uint8_t> display_init_bytes;
static St7123Config st7123_config {};
static Device display_device {};
static St7123TouchConfig st7123_touch_config {};
static Device st7123_touch_device {};
static void create_st7123_touch(Device* i2c0) {
st7123_touch_device = Device {
.address = 0,
.name = "touch0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
GpioPinSpec pin_interrupt = GPIO_PIN_SPEC_NONE;
Device* gpio0 = nullptr;
if (device_get_by_name("gpio0", &gpio0) == ERROR_NONE) {
pin_interrupt = GpioPinSpec { gpio0, 23, GPIO_FLAG_NONE };
device_put(gpio0);
} else {
LOG_W(TAG, "display_detect: gpio0 not found, touch interrupt pin will not be wired");
}
st7123_touch_config = St7123TouchConfig {
.address = 0x55, // fixed - see ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS
.x_max = 720,
.y_max = 1280,
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
// Reset is pulsed via io_expander0 (detect.cpp's pulse_display_reset_pins), not a direct SoC GPIO.
.pin_reset = GPIO_PIN_SPEC_NONE,
.pin_interrupt = pin_interrupt,
};
st7123_touch_device.config = &st7123_touch_config;
construct_add_start(&st7123_touch_device, i2c0, "sitronix,st7123-touch");
}
void tab5_create_devices_v2(Device* i2c0) {
display_device = Device {
.address = 0,

View File

@ -0,0 +1,48 @@
#include "devices_v2_v3_touch.h"
#include "devices_common.h"
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
#include <tactility/log.h>
#include <drivers/st7123_touch.h>
constexpr auto* TAG = "Tab5";
static St7123TouchConfig st7123_touch_config {};
static Device st7123_touch_device {};
void create_st7123_touch(Device* i2c0) {
st7123_touch_device = Device {
.address = 0,
.name = "touch0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
GpioPinSpec pin_interrupt = GPIO_PIN_SPEC_NONE;
Device* gpio0 = nullptr;
if (device_get_by_name("gpio0", &gpio0) == ERROR_NONE) {
pin_interrupt = GpioPinSpec { gpio0, 23, GPIO_FLAG_NONE };
device_put(gpio0);
} else {
LOG_W(TAG, "display_detect: gpio0 not found, touch interrupt pin will not be wired");
}
st7123_touch_config = St7123TouchConfig {
.address = 0x55, // fixed - see ESP_LCD_TOUCH_IO_I2C_ST7123_ADDRESS
.x_max = 720,
.y_max = 1280,
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
// Reset is pulsed via io_expander0 (detect.cpp's pulse_display_reset_pins), not a direct SoC GPIO.
.pin_reset = GPIO_PIN_SPEC_NONE,
.pin_interrupt = pin_interrupt,
};
st7123_touch_device.config = &st7123_touch_config;
construct_add_start(&st7123_touch_device, i2c0, "sitronix,st7123-touch");
}

View File

@ -0,0 +1,8 @@
#pragma once
struct Device;
// Constructs, parents, binds and starts the ST7123 in-cell touch device on i2c0. Shared by the V2
// (ST7123 display) and V3 (ST7121 display) variants - both boards use the exact same touch
// controller/address/wiring, only the display panel differs. Not used by V1 (ILI9881C + GT911).
void create_st7123_touch(Device* i2c0);

View File

@ -0,0 +1,85 @@
#include "devices_v3.h"
#include "devices_common.h"
#include "devices_v2_v3_touch.h"
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
#include <tactility/log.h>
#include <drivers/st7121.h>
#define TAG "Tab5"
static St7121Config st7121_config {};
static Device display_device {};
// Newest Tab5 variant: ST7121 display + the same in-cell ST7123 touch controller as V2 (see
// devices_common.cpp's create_st7123_touch). No custom init-sequence is supplied - unlike ST7123,
// the M5Tab5 UserDemo runs the ST7121 panel with the esp_lcd_st7121 component's own built-in
// default bring-up sequence (see esp_lcd_st7121.c's vendor_specific_init_default), so
// init_sequence stays null here too. Timing values (vsync_pulse_width/back_porch/front_porch) are
// per the UserDemo's is_st7121 branch in bsp_display_new_with_handles_to_st7123() - the only
// values that differ from V2/ST7123.
void tab5_create_devices_v3(Device* i2c0) {
display_device = Device {
.address = 0,
.name = "display0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
Device* backlight = nullptr;
if (device_get_by_name("display_backlight", &backlight) != ERROR_NONE) {
LOG_W(TAG, "display_detect: display_backlight not found");
}
st7121_config = St7121Config {
.horizontal_resolution = 720,
.vertical_resolution = 1280,
.bits_per_pixel = 16,
.bgr_order = false,
.invert_color = false,
.mirror_x = false,
.mirror_y = false,
.pin_reset = GPIO_PIN_SPEC_NONE,
.ldo_channel = 3,
.ldo_voltage_mv = 2500,
.dsi_bus_id = 0,
.num_data_lanes = 2,
.lane_bit_rate_mbps = 965, // ST7121 lane bitrate per M5Stack BSP (same as ST7123)
.dpi_clock_freq_mhz = 70,
.hsync_pulse_width = 2,
.hsync_back_porch = 40,
.hsync_front_porch = 40,
.vsync_pulse_width = 20,
.vsync_back_porch = 24,
.vsync_front_porch = 200,
.num_fbs = 2,
.use_dma2d = true,
.disable_lp = false,
.allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 (disabled = don't wait)
.init_sequence = nullptr, // use esp_lcd_st7121's built-in default sequence, per the UserDemo
.init_sequence_length = 0,
.backlight = backlight,
};
display_device.config = &st7121_config;
if (backlight != nullptr) {
device_put(backlight);
}
Device* root = nullptr;
if (device_get_by_name("/", &root) != ERROR_NONE) {
LOG_E(TAG, "display_detect: root device not found");
return;
}
bool started = construct_add_start(&display_device, root, "sitronix,st7121");
device_put(root);
if (!started) {
return;
}
create_st7123_touch(i2c0);
}

View File

@ -0,0 +1,5 @@
#pragma once
struct Device;
void tab5_create_devices_v3(Device* i2c0);

View File

@ -3,6 +3,7 @@
#include "devices_common.h"
#include "devices_v1.h"
#include "devices_v2.h"
#include "devices_v3.h"
#include "tab5_keyboard.h"
#include <tactility/device.h>
@ -91,16 +92,18 @@ static void on_display_detect_event(Device* device, DeviceEvent event, void* con
}
}
if (io_expander0 != nullptr && tab5_get_variant() == Tab5Variant::Unknown) {
Tab5Variant variant = tab5_probe_variant(i2c0);
tab5_set_variant(variant);
}
// We need i2c0 and io_expander0 to create the display and touch devices
// We need i2c0 and io_expander0 to pulse the LCD/touch reset pins, probe the variant, and
// create the display and touch devices - all gated on the same pair, so do them together in
// one pass. Order matters: the reset pulse must run *before* tab5_probe_variant(), otherwise
// the touch IC's response depends on whatever power-on state it happened to be in rather than
// a deterministic post-reset state.
if (!did_create_display && i2c0 != nullptr && io_expander0 != nullptr) {
did_create_display = true;
if (pulse_display_reset_pins(io_expander0)) {
switch (tab5_get_variant()) {
Tab5Variant variant = tab5_probe_variant(i2c0);
tab5_set_variant(variant);
switch (variant) {
case Tab5Variant::Unknown:
LOG_E(TAG, "Variant not detected yet");
break;
@ -110,6 +113,9 @@ static void on_display_detect_event(Device* device, DeviceEvent event, void* con
case Tab5Variant::V2:
tab5_create_devices_v2(i2c0);
break;
case Tab5Variant::V3:
tab5_create_devices_v3(i2c0);
break;
}
} else {
LOG_E(TAG, "display_detect: skipping display creation, failed to pulse reset pins");

View File

@ -9,7 +9,6 @@
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <lvgl/lvgl.h>
#include <tactility/module.h>
#include <driver/gpio.h>
@ -36,11 +35,6 @@ static constexpr uint32_t REPEAT_RATE_MS = 80;
// REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
static constexpr uint32_t POLL_INTERVAL_MS = 20;
// 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 (see check_attach_state()).
static constexpr uint32_t ATTACH_CHECK_INTERVAL_MS = 1000;
// ---------------------------------------------------------------------------
// Register addresses
// ---------------------------------------------------------------------------
@ -193,14 +187,10 @@ struct Tab5KeyboardInternal {
bool irq_configured;
gpio_num_t irq_pin;
// Poll/attach-check throttling (real-time based, since read_key() is called at whatever rate
// LVGL's indev timer and its own drain-loop - via continue_reading - happen to run at, unlike
// the old deprecated-HAL's fixed 20ms Timer)
// Poll throttling (real-time based, since read_key() is called at whatever rate LVGL's indev
// timer and its own drain-loop - via continue_reading - happen to run at, unlike the old
// deprecated-HAL's fixed 20ms Timer)
uint32_t last_poll_ms;
uint32_t last_attach_check_ms;
bool was_attached;
bool pending_attach_state;
uint8_t pending_attach_confirm_count;
// Software key-repeat state (tracked by position to survive modifier changes)
uint32_t repeat_key;
@ -208,9 +198,6 @@ struct Tab5KeyboardInternal {
uint8_t repeat_col;
uint32_t repeat_start_ms;
uint32_t repeat_last_ms;
Tab5KeyboardAttachListener attach_listener;
void* attach_listener_context;
};
// ---------------------------------------------------------------------------
@ -226,9 +213,20 @@ static bool write_reg(Device* device, uint8_t reg, uint8_t value) {
return i2c_controller_write_register(parent, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
static bool is_attached_raw(Device* device) {
// Short-timeout variant used only by tab5_keyboard_reinit(), which runs on the FreeRTOS timer
// daemon task (via tab5_keyboard_attach_detect.cpp) - a slow/absent device there blocks every
// other software timer in the system, not just this one, so it can't afford write_reg()'s 50ms
// per-call budget. read_reg()/write_reg() themselves stay at 50ms since they're also used from the
// hot IRQ/poll path (drain_events()), where a too-short timeout would cause missed key events
// under normal bus contention.
static bool write_reg_fast(Device* device, uint8_t reg, uint8_t value) {
auto* parent = device_get_parent(device);
return i2c_controller_has_device_at_address(parent, I2C_ADDRESS, pdMS_TO_TICKS(100)) == 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) {
auto* parent = device_get_parent(device);
return i2c_controller_has_device_at_address(parent, I2C_ADDRESS, pdMS_TO_TICKS(5)) == ERROR_NONE;
}
// ---------------------------------------------------------------------------
@ -385,69 +383,32 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
}
// ---------------------------------------------------------------------------
// reinit_device - (re)applies the device register configuration. Used at start() and again on
// hot-plug reattach, since the device's RGB mode and interrupt configuration are volatile and
// reset to power-on defaults when the keyboard is unplugged and reconnected.
// tab5_keyboard_reinit - (re)applies the device register configuration. Called from start() and
// again by tab5_keyboard_attach_detect.cpp on confirmed hot-plug reattach, since the device's RGB
// mode and interrupt configuration are volatile and reset to power-on defaults when the keyboard
// is unplugged and reconnected.
// ---------------------------------------------------------------------------
static void reinit_device(Device* device, Tab5KeyboardInternal* internal) {
write_reg(device, REG_KEYBOARD_MODE, 0x00); // Normal mode
write_reg(device, REG_EVENT_NUM, 0x00); // flush event queue
write_reg(device, REG_INT_STAT, 0x00); // clear pending INT
write_reg(device, REG_RGB_MODE, 0x01); // Custom RGB mode (manual LED control)
write_reg(device, REG_BRIGHTNESS, 50); // 50% brightness
update_leds(device, internal); // restore current LED state
void tab5_keyboard_reinit(Device* 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_EVENT_NUM, 0x00); // flush event queue
write_reg_fast(device, REG_INT_STAT, 0x00); // clear pending INT
write_reg_fast(device, REG_RGB_MODE, 0x01); // Custom RGB mode (manual LED control)
write_reg_fast(device, REG_BRIGHTNESS, 50); // 50% brightness
update_leds(device, internal); // restore current LED state
if (internal->irq_configured) {
write_reg(device, REG_INT_CFG, 0x01); // re-enable Normal-mode interrupt (bit 0)
write_reg_fast(device, REG_INT_CFG, 0x01); // re-enable Normal-mode interrupt (bit 0)
}
}
// ---------------------------------------------------------------------------
// check_attach_state - throttled (~1s) hot-plug detection. Reapplies device register
// configuration on reattach, and notifies the registered attach listener (if any) of confirmed
// transitions - see Tab5KeyboardAttachListener's doc comment for the retry contract.
// ---------------------------------------------------------------------------
static void check_attach_state(Device* device, Tab5KeyboardInternal* internal) {
uint32_t now = now_ms();
if (now - internal->last_attach_check_ms < ATTACH_CHECK_INTERVAL_MS) {
return;
}
internal->last_attach_check_ms = now;
const bool attached = is_attached_raw(device);
if (attached == internal->was_attached) {
internal->pending_attach_confirm_count = 0;
return;
}
// 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 != internal->pending_attach_state || internal->pending_attach_confirm_count == 0) {
internal->pending_attach_state = attached;
internal->pending_attach_confirm_count = 1;
return;
}
internal->pending_attach_confirm_count = 0;
if (attached) {
reinit_device(device, internal);
}
if (internal->attach_listener != nullptr) {
if (!internal->attach_listener(device, attached, internal->attach_listener_context)) {
return; // not handled yet (e.g. LVGL lock busy) - retry on the next confirmed check
}
}
internal->was_attached = attached;
}
// ---------------------------------------------------------------------------
// poll_if_due - the closest equivalent to the old deprecated-HAL's 20ms-Timer-driven
// processKeyboard(): drains new key events (IRQ-gated or polled), ticks software key-repeat, and
// checks hot-plug attach state. Called from read_key(), throttled to real elapsed time rather
// than call count, since read_key() can be called back-to-back multiple times per LVGL indev
// timer tick while draining an already-queued burst (continue_reading).
// processKeyboard(): drains new key events (IRQ-gated or polled) and ticks software key-repeat.
// Called from read_key(), throttled to real elapsed time rather than call count, since read_key()
// can be called back-to-back multiple times per LVGL indev timer tick while draining an
// already-queued burst (continue_reading). Hot-plug attach detection lives outside the driver -
// see tab5_keyboard_attach_detect.cpp.
// ---------------------------------------------------------------------------
static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) {
uint32_t now = now_ms();
@ -483,8 +444,6 @@ static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) {
}
}
}
check_attach_state(device, internal);
}
static gpio_num_t pin_or_nc(const GpioPinSpec& pin) {
@ -517,17 +476,24 @@ static error_t start(Device* device) {
internal->irq_pin = pin_or_nc(config->pin_interrupt);
if (internal->irq_pin != GPIO_NUM_NC) {
configure_irq_pin(internal); // best-effort; falls back to polling if it fails. Must
// happen before reinit_device() so REG_INT_CFG is written
// if IRQ setup succeeded.
// happen before tab5_keyboard_reinit() so REG_INT_CFG is
// written if IRQ setup succeeded.
}
// Best-effort: if the keyboard isn't attached yet (e.g. this device is constructed
// speculatively at boot so it can be hot-plug-detected later), these I2C writes fail
// silently and reinit_device() runs again once attach is detected.
reinit_device(device, internal);
internal->was_attached = is_attached_raw(device);
// Driver data must be set before tab5_keyboard_reinit() - it looks internal back up via
// device_get_driver_data().
device_set_driver_data(device, internal);
// This device is constructed speculatively at boot so it can be hot-plug-detected later - if
// the keyboard isn't physically attached yet, skip reinit here (tab5_keyboard_attach_detect.cpp
// calls it again once attach is confirmed) rather than issuing register writes that are certain
// 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;
}
@ -591,76 +557,17 @@ Driver tab5_keyboard_driver = {
.internal = nullptr
};
// region Attach listener
void tab5_keyboard_add_attach_listener(Device* device, Tab5KeyboardAttachListener callback, void* context) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
if (internal->attach_listener != nullptr) {
LOG_W(TAG, "Replacing existing attach listener without it being removed first");
}
internal->attach_listener = callback;
internal->attach_listener_context = context;
}
void tab5_keyboard_remove_attach_listener(Device* device, Tab5KeyboardAttachListener callback) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
if (internal->attach_listener != callback) {
return;
}
internal->attach_listener = nullptr;
internal->attach_listener_context = nullptr;
}
// endregion
// region Dynamic construction
// Reacts to the Tab5 keyboard accessory's hot-plug attach state (see Tab5KeyboardAttachListener's
// doc comment above for the retry contract). This is UI-layer behavior the driver itself can't do
// (it has no LVGL dependency): switch to landscape while the keyboard is attached, restoring
// whatever rotation was active before once it's removed - but only if the user hasn't manually
// changed it since attaching, in which case their choice is respected. Ported as-is from the
// deprecated HAL's Tab5Keyboard::applyAutoRotation().
static bool on_keyboard_attach_changed(Device* /*device*/, bool attached, void* /*context*/) {
static lv_display_rotation_t saved_rotation = LV_DISPLAY_ROTATION_0;
static bool rotation_override_active = false;
auto* display = lv_display_get_default();
if (display == nullptr) {
return false; // LVGL not ready yet - retry on the next confirmed check
}
if (!lvgl_try_lock(pdMS_TO_TICKS(1000))) {
return false; // retry next check
}
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 {
// 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 Tab5KeyboardConfig tab5_keyboard_config {};
static Device tab5_keyboard_device {};
// 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
// all (it lives on i2c2, a separate bus) - lvgl-module binds its indev unconditionally at boot
// regardless of physical attach state, and the driver's own read_key() polling handles hot-plug
// internally.
// regardless of physical attach state. Hot-plug attach/detach handling (register reinit, LVGL
// rotation) lives in tab5_keyboard_attach_detect.cpp, not this driver - see module.cpp for where
// that gets started.
void tab5_create_keyboard(Device* i2c2) {
tab5_keyboard_device = Device {
.address = 0,
@ -687,9 +594,7 @@ void tab5_create_keyboard(Device* i2c2) {
// Parented to i2c2 itself (not root, unlike the display): the keyboard driver's start() uses
// device_get_parent() as its I2C bus controller.
if (construct_add_start(&tab5_keyboard_device, i2c2, "m5stack,tab5-keyboard")) {
tab5_keyboard_add_attach_listener(&tab5_keyboard_device, on_keyboard_attach_changed, nullptr);
}
construct_add_start(&tab5_keyboard_device, i2c2, "m5stack,tab5-keyboard");
}
// endregion

View File

@ -20,27 +20,23 @@ struct Tab5KeyboardConfig {
struct GpioPinSpec pin_interrupt;
};
// Called when the keyboard accessory's hot-plug attach state changes (confirmed over two
// consecutive ~1s checks - see the driver source). Orientation changes and other LVGL-aware
// reactions to attach state live outside the driver (it has no LVGL dependency) - module.cpp
// registers a listener for this instead.
// @return true once handled; false to be called again on the next confirmed check with the same
// `attached` value (e.g. if the LVGL lock couldn't be acquired) - mirrors this driver's own
// internal retry-until-handled pattern for reinitializing the device on reattach.
typedef bool (*Tab5KeyboardAttachListener)(struct Device* device, bool attached, void* context);
// Only one listener is supported (this board only ever has one caller - module.cpp). Registering
// a new one before removing the previous replaces it with a warning logged.
void tab5_keyboard_add_attach_listener(struct Device* device, Tab5KeyboardAttachListener callback, void* context);
void tab5_keyboard_remove_attach_listener(struct Device* device, Tab5KeyboardAttachListener callback);
extern struct Driver tab5_keyboard_driver;
// Constructs and starts the keyboard accessory device on i2c2, then registers this project's own
// hot-plug rotation handler as its attach listener. Called from display_detect.cpp's
// Constructs and starts the keyboard accessory device on i2c2. Called from display_detect.cpp's
// on_display_detect_event() once i2c2 is up.
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);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,157 @@
#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 {
// 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 2-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) {
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;
}

View File

@ -0,0 +1,12 @@
#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();

View File

@ -13,6 +13,7 @@
#include "devices/detect.h"
#include "devices/tab5_headphone_detect.h"
#include "devices/tab5_keyboard.h"
#include "devices/tab5_keyboard_attach_detect.h"
#include "devices/tab5_power_control.h"
#include "devices/tab_5_camera.h"
@ -69,10 +70,12 @@ static error_t start() {
tab5_camera_init();
device_listener_add(on_io_expander0_started, nullptr);
tab5_headphone_detect_start();
tab5_keyboard_attach_detect_start();
return ERROR_NONE;
}
static error_t stop() {
tab5_keyboard_attach_detect_stop();
tab5_headphone_detect_stop();
device_listener_remove(on_io_expander0_started);
tab5_detect_stop();

View File

@ -10,5 +10,6 @@ dependencies:
- Drivers/sc2356-module
- Drivers/ili9881c-module
- Drivers/st7123-module
- Drivers/st7121-module
- Drivers/gt911-module
dts: m5stack,tab5.dts

View File

@ -71,12 +71,12 @@
pin-sda = <&gpio0 31 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 32 GPIO_FLAG_PULL_UP>;
io_expander0: io_expander0 {
io_expander0 {
compatible = "diodes,pi4ioe5v6408";
reg = <0x43>;
};
io_expander1: io_expander1 {
io_expander1 {
compatible = "diodes,pi4ioe5v6408";
reg = <0x44>;
};
@ -123,6 +123,11 @@
sc2356 {
compatible = "smartsens,sc2356";
reg = <0x36>;
// Pulsed by the sc2356 driver itself in start_device, not a gpio-hog: the driver's
// I2C probe otherwise races a bare hog's pin release with no settle delay (the hog
// also only starts after this node, since it's a root-level sibling declared later
// in this file - always too late).
pin-reset = <&io_expander0 6 GPIO_FLAG_ACTIVE_LOW>;
};
};
@ -138,12 +143,6 @@
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
exp0_camera_reset {
compatible = "gpio-hog";
pin = <&io_expander0 6 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
exp1_c6_wlan_enable {
compatible = "gpio-hog";
pin = <&io_expander1 0 GPIO_FLAG_NONE>;

View File

@ -3,3 +3,13 @@ description: Smartsens SC2356 2MP MIPI CSI camera sensor
include: [ "i2c-device.yaml" ]
compatible: "smartsens,sc2356"
properties:
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: >
Reset GPIO pin. Pulsed (asserted then released, with a settle delay) before the sensor is
probed in start_device. Typically wired through a board's IO expander rather than a direct
SoC GPIO, so it's handled here via the generic gpio_descriptor API rather than
esp_video_init's csi_config.reset_pin (which only accepts native SoC GPIOs).

View File

@ -5,6 +5,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <tactility/drivers/camera.h>
#include <tactility/drivers/gpio.h>
#include <tactility/error.h>
struct Device;
@ -16,6 +17,11 @@ extern "C" {
struct Sc2356Config {
/** SCCB I2C address (0x36) */
uint8_t address;
// Reset pin. GPIO_PIN_SPEC_NONE if the sensor's reset is tied high on the board (or otherwise
// not under our control), in which case start_device skips the reset pulse entirely and goes
// straight to probing.
struct GpioPinSpec pin_reset;
};
/**

View File

@ -4,6 +4,7 @@
#include <tactility/device.h>
#include <tactility/drivers/esp32_i2c_master.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
@ -63,6 +64,19 @@ struct Sc2356State : CameraHandleData {
#define GET_CONFIG(device) (static_cast<const Sc2356Config*>((device)->config))
// Reset timings per the SC2356/SC202CS datasheet's power-up sequence: hold reset asserted for at
// least 1ms, then wait for the sensor's internal power-on/clock startup (datasheet specifies a
// minimum before the first SCCB transaction - 10ms gives comfortable margin) before probing.
static error_t reset_pulse(GpioDescriptor* descriptor) {
// Release is attempted unconditionally, even if assert failed - leaving the expander output
// asserted on an assert failure would hold the sensor in reset for the rest of boot.
const bool assert_ok = gpio_descriptor_set_level(descriptor, true) == ERROR_NONE; // assert
vTaskDelay(pdMS_TO_TICKS(10));
const bool release_ok = gpio_descriptor_set_level(descriptor, false) == ERROR_NONE; // release
vTaskDelay(pdMS_TO_TICKS(10));
return (assert_ok && release_ok) ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t start(Device* device) {
auto* i2c = device_get_parent(device);
if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) {
@ -70,7 +84,28 @@ static error_t start(Device* device) {
return ERROR_RESOURCE;
}
auto address = GET_CONFIG(device)->address;
const auto* config = GET_CONFIG(device);
auto address = config->address;
// Reset is pulsed (not just held released) so the sensor reaches a known state regardless of
// whatever it inherited from a previous boot - see the equivalent reasoning for the tab5
// display/touch reset pulse in Devices/m5stack-tab5/Source/devices/display_detect.cpp.
if (config->pin_reset.gpio_controller != nullptr) {
// Merge with the devicetree-supplied flags (e.g. GPIO_FLAG_ACTIVE_LOW) rather than
// overwriting them, so a board that wires reset active-high isn't silently forced to
// active-low polarity.
auto* reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, config->pin_reset.flags | GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO);
if (reset_descriptor == nullptr) {
LOG_E(TAG, "Failed to acquire reset pin");
return ERROR_RESOURCE;
}
error_t reset_error = reset_pulse(reset_descriptor);
gpio_descriptor_release(reset_descriptor);
if (reset_error != ERROR_NONE) {
LOG_E(TAG, "Failed to pulse reset pin");
return reset_error;
}
}
uint8_t chip_id_h = 0, chip_id_l = 0;
error_t err = i2c_controller_write_read(i2c, address, SC2356_REG_CHIP_ID_H, sizeof(SC2356_REG_CHIP_ID_H), &chip_id_h, 1, I2C_TIMEOUT);

View File

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(st7121-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_lcd_st7121 esp_lcd driver esp_hw_support
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,12 @@
# ST7121 display controller
Driver for the Sitronix `ST7121` MIPI-DSI display panel (`sitronix,st7121`), driven over ESP-IDF's
`esp_lcd_st7121` component (DPI/DBI interface, ESP32-P4 and other `SOC_MIPI_DSI_SUPPORTED` targets
only). Owns the MIPI DSI PHY LDO channel and DSI bus directly, so unlike SPI/RGB panels it has no
parent bus controller device.
The ST7121 panel's in-cell touch controller is the same chip/protocol as the ST7123's (see
`st7123-module`'s `sitronix,st7123-touch` driver) - boards using this display reuse that driver
for touch rather than duplicating it here.
License: [Apache v2.0](LICENSE-Apache-2.0.md)

View File

@ -0,0 +1,111 @@
description: >
Sitronix ST7121 MIPI-DSI display panel, driven over ESP-IDF's esp_lcd_st7121 component (DPI/DBI interface, ESP32-P4 and other SOC_MIPI_DSI_SUPPORTED targets only). Owns the MIPI DSI PHY LDO channel and DSI bus directly, so unlike SPI/RGB panels it has no parent bus controller device.
compatible: "sitronix,st7121"
properties:
horizontal-resolution:
type: int
required: true
description: Horizontal resolution in pixels
vertical-resolution:
type: int
required: true
description: Vertical resolution in pixels
bits-per-pixel:
type: int
default: 16
description: Color depth in bits per pixel (16, 18 or 24)
bgr-order:
type: boolean
default: false
description: Use BGR element order instead of RGB
invert-color:
type: boolean
default: false
description: Invert the panel's color output
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin. Falls back to a software reset (sent over the DBI command interface) if not set.
ldo-channel:
type: int
required: true
description: LDO channel index powering the MIPI DSI PHY (chan_id in esp_ldo_channel_config_t)
ldo-voltage-mv:
type: int
required: true
description: Voltage to supply to the MIPI DSI PHY LDO channel, in mV
dsi-bus-id:
type: int
default: 0
description: Which DSI controller to use, index from 0
num-data-lanes:
type: int
default: 2
description: Number of MIPI DSI data lanes. 0 falls back to the maximum available.
lane-bit-rate-mbps:
type: int
required: true
description: MIPI DSI lane bit rate, in Mbps
dpi-clock-freq-mhz:
type: int
required: true
description: MIPI DPI clock frequency, in MHz
hsync-pulse-width:
type: int
required: true
description: Horizontal sync width, in PCLK periods
hsync-back-porch:
type: int
required: true
description: Number of PCLK periods between hsync and the start of line active data
hsync-front-porch:
type: int
required: true
description: Number of PCLK periods between the end of active data and the next hsync
vsync-pulse-width:
type: int
required: true
description: Vertical sync width, in lines
vsync-back-porch:
type: int
required: true
description: Number of invalid lines between vsync and the start of the frame
vsync-front-porch:
type: int
required: true
description: Number of invalid lines between the end of the frame and the next vsync
num-fbs:
type: int
default: 1
description: Number of screen-sized frame buffers to allocate (0 or 1 = single-buffered)
use-dma2d:
type: boolean
default: true
description: Use DMA2D to copy user buffers into the frame buffer when necessary (only meaningful on SOC_DMA2D_SUPPORTED targets - must be false otherwise)
disable-lp:
type: boolean
default: false
description: Disable MIPI DSI low-power mode
allow-tearing:
type: boolean
default: false
description: By default, when draw_bitmap's color_data is one of the panel's own frame buffers (i.e. LVGL is bound directly onto them), draw_bitmap waits for a full scan-out to complete before returning so the caller can't start overwriting a buffer still being displayed. Set this to trade away that tear-free guarantee for lower latency (e.g. when other tasks occasionally block timing for long enough that waiting causes visible stalls).
init-sequence:
type: array
element-type: uint8_t
description: >
Custom vendor bring-up sequence, flattened into bytes as a run of [cmd, data-length, delay-ms, data-length bytes of data...] entries, e.g. `init-sequence = [0x11 0 120 0x29 0 20];`. Omit to use the ST7121 component's own built-in default sequence.
backlight:
type: phandle
default: "NULL"
description: Optional reference to this display's backlight device

View File

@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings

View File

@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/st7121.h>
// The devicetree compiler derives the expected config typedef name from the compatible
// string's suffix (e.g. "sitronix,st7121" -> st7121_config_dt), not from the node name or
// driver name, so the tag here must match that exactly.
DEFINE_DEVICETREE(st7121, struct St7121Config)

View File

@ -0,0 +1,68 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
struct St7121Config {
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
uint8_t bits_per_pixel;
bool bgr_order;
bool invert_color;
bool mirror_x;
bool mirror_y;
// Reset pin for the panel. GPIO_PIN_SPEC_NONE falls back to a software reset sent over the
// DBI command interface (see esp_lcd_st7121's panel_st7121_reset()).
struct GpioPinSpec pin_reset;
// LDO channel powering the MIPI DSI PHY - the PHY has no power of its own until this is
// acquired, so it must happen before the DSI bus is created. Both fields are int32_t (not
// uint32_t) to exactly match esp_ldo_channel_config_t's plain `int` fields - assigning a
// uint32_t into that struct's brace-init would be a narrowing conversion (-Werror=narrowing).
int32_t ldo_channel;
int32_t ldo_voltage_mv;
uint8_t dsi_bus_id;
// Number of MIPI DSI data lanes. 0 falls back to the maximum available.
uint8_t num_data_lanes;
uint32_t lane_bit_rate_mbps;
uint32_t dpi_clock_freq_mhz;
uint32_t hsync_pulse_width;
uint32_t hsync_back_porch;
uint32_t hsync_front_porch;
uint32_t vsync_pulse_width;
uint32_t vsync_back_porch;
uint32_t vsync_front_porch;
// Number of screen-sized frame buffers the driver allocates (0 or 1 = single-buffered).
uint8_t num_fbs;
bool use_dma2d;
bool disable_lp;
// See the 'allow-tearing' binding property.
bool allow_tearing;
// Custom vendor init sequence, flattened as bytes: a run of
// [cmd, data_len, delay_ms, data_len bytes of data...] entries. NULL/0 falls back to the
// ST7121 component's own built-in default sequence (see vendor_specific_init_default in
// esp_lcd_st7121.c) - not guaranteed to match any particular panel's actual bring-up
// requirements.
const uint8_t* init_sequence;
uint32_t init_sequence_length;
// Optional reference to this display's backlight device, NULL if none.
struct Device* backlight;
};
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module st7121_module;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver st7121_driver;
static Driver* const st7121_drivers[] = {
&st7121_driver,
nullptr
};
Module st7121_module = {
.name = "st7121",
.drivers = st7121_drivers
};
} // extern "C"

View File

@ -0,0 +1,590 @@
// SPDX-License-Identifier: Apache-2.0
#include <soc/soc_caps.h>
#if SOC_MIPI_DSI_SUPPORTED
#include <drivers/st7121.h>
#include <st7121_module.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <esp_err.h>
#include <esp_ldo_regulator.h>
#include <esp_lcd_st7121.h>
#include <esp_lcd_mipi_dsi.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstdlib>
constexpr auto* TAG = "ST7121";
#define GET_CONFIG(device) (static_cast<const St7121Config*>((device)->config))
// Generic lvgl-module display glue (Modules/lvgl-module/source/lvgl_display.c) only ever asks
// for frame buffer index 0 and 1, so caching more than that would be dead weight.
constexpr size_t MAX_CACHED_FRAME_BUFFERS = 2;
struct St7121Internal {
esp_ldo_channel_handle_t ldo_handle;
esp_lcd_dsi_bus_handle_t dsi_bus_handle;
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_handle_t panel_handle;
void* frame_buffers[MAX_CACHED_FRAME_BUFFERS];
uint8_t frame_buffer_count;
// Size of each buffer in frame_buffers, in bytes - used to range-check whether a given
// draw_bitmap() color_data pointer is actually one of them (see draw_bitmap() below).
size_t frame_buffer_size_bytes;
// Signaled by on_refresh_done once per real scan-out of a whole frame. Only waited on in
// draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the
// comment there for why.
SemaphoreHandle_t frame_complete_semaphore;
// Signaled by on_color_trans_done once the panel driver's own copy/DMA2D transfer of a
// draw_bitmap() call's pixels into the target frame buffer completes. Unlike
// frame_complete_semaphore this is always waited on (regardless of allow_tearing or whether
// color_data is a real frame buffer) - see the comment on draw_bitmap() for why.
SemaphoreHandle_t color_trans_done_semaphore;
// Heap-allocated only when the devicetree supplies a custom init_sequence (see
// parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence
// needs no parsing. Its .data pointers alias directly into the devicetree's static const
// byte buffer, so only this struct array itself needs freeing in stop().
st7121_lcd_init_cmd_t* parsed_init_cmds;
};
// esp_lcd_dpi_panel's draw_bitmap() has a zero-copy path when color_data is one of the panel's
// own frame buffers (as returned by esp_lcd_dpi_panel_get_frame_buffer()): it just repoints which
// buffer is scanned out and returns almost instantly - well before the DSI peripheral has
// actually finished scanning out the *previous* buffer, let alone started on this one. Callers in
// full/direct LVGL render mode render straight into these real frame buffers, so if draw_bitmap()
// returned that quickly, LVGL would be free to start overwriting the *other* buffer - which may
// still be mid-scanout - producing visible tearing/flashing. on_refresh_done fires once per actual
// whole-frame scan-out completion (continuously, at the panel's refresh rate, independent of
// draw_bitmap calls), so waiting for the next occurrence after each draw_bitmap() genuinely blocks
// until it's safe to start writing into the frame buffers again - unless avoid_tearing is false,
// in which case the caller has opted out of this wait (see the binding property).
static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) {
auto* internal = static_cast<St7121Internal*>(user_ctx);
BaseType_t high_task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->frame_complete_semaphore, &high_task_woken);
return high_task_woken == pdTRUE;
}
// esp_lcd_dpi_panel_draw_bitmap() copies (memcpy, or DMA2D when use_dma2d is set) the caller's
// pixels into the target frame buffer and returns immediately - the copy itself finishes
// asynchronously. Calling draw_bitmap() again before that copy completes hits the panel driver's
// own re-entrancy guard and logs "previous draw operation is not finished" (lcd.dsi). This is
// unconditional, unlike frame_complete_semaphore's tearing-avoidance wait: it protects the copy
// itself, not scan-out, so it must be waited on regardless of allow_tearing or which buffer
// color_data is.
//
// Unlike on_refresh_done (always ISR, from the VSYNC interrupt), esp_lcd_panel_dpi.c invokes this
// callback from two different contexts depending on the copy path: synchronously from the calling
// task for the CPU-memcpy and fb-direct (zero-copy) paths, but from an ISR (async_fbcpy_done_cb,
// the DMA2D completion interrupt) when use_dma2d is set. Giving a semaphore with the ISR-only API
// from task context is undefined behavior on FreeRTOS, so the context must be checked at runtime.
static bool on_color_trans_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) {
auto* internal = static_cast<St7121Internal*>(user_ctx);
if (xPortInIsrContext()) {
BaseType_t high_task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->color_trans_done_semaphore, &high_task_woken);
return high_task_woken == pdTRUE;
}
xSemaphoreGive(internal->color_trans_done_semaphore);
return false;
}
static int pin_or_unused(const GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
}
static lcd_color_format_t color_format_from_bits_per_pixel(uint8_t bits_per_pixel) {
switch (bits_per_pixel) {
case 18: return LCD_COLOR_FMT_RGB666;
case 24: return LCD_COLOR_FMT_RGB888;
default: return LCD_COLOR_FMT_RGB565;
}
}
// Unpacks the devicetree's flat [cmd, data_len, delay_ms, data_len bytes...] encoding (produced
// by the devicetree compiler's "array" property type - see init-sequence in
// bindings/sitronix,st7121.yaml) into a heap-allocated st7121_lcd_init_cmd_t array. Each entry's
// .data points directly into `bytes`, which is the devicetree's static const buffer and outlives
// the device, so no per-entry copy is needed.
static bool parse_init_sequence(const uint8_t* bytes, uint32_t length, st7121_lcd_init_cmd_t** out_cmds, uint16_t* out_count) {
uint32_t count = 0;
for (uint32_t offset = 0; offset < length; count++) {
if (offset + 3 > length) {
LOG_E(TAG, "init-sequence truncated: entry header runs past the end of the array");
return false;
}
offset += 3 + bytes[offset + 1];
if (offset > length) {
LOG_E(TAG, "init-sequence truncated: entry data runs past the end of the array");
return false;
}
}
auto* cmds = static_cast<st7121_lcd_init_cmd_t*>(malloc(count * sizeof(st7121_lcd_init_cmd_t)));
if (cmds == nullptr) {
return false;
}
uint32_t offset = 0;
for (uint32_t i = 0; i < count; i++) {
uint8_t data_len = bytes[offset + 1];
cmds[i] = {
.cmd = bytes[offset],
.data = data_len > 0 ? &bytes[offset + 3] : nullptr,
.data_bytes = data_len,
.delay_ms = bytes[offset + 2],
};
offset += 3 + data_len;
}
*out_cmds = cmds;
*out_count = (uint16_t)count;
return true;
}
// region Driver lifecycle
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<St7121Internal*>(malloc(sizeof(St7121Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->parsed_init_cmds = nullptr;
const st7121_lcd_init_cmd_t* init_cmds = nullptr;
uint16_t init_cmds_size = 0;
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
if (!parse_init_sequence(config->init_sequence, config->init_sequence_length, &internal->parsed_init_cmds, &init_cmds_size)) {
LOG_E(TAG, "Failed to parse init-sequence property");
free(internal);
return ERROR_INVALID_ARGUMENT;
}
init_cmds = internal->parsed_init_cmds;
}
// The MIPI DSI PHY has no power of its own until this LDO channel is enabled - must happen
// before the DSI bus is created.
esp_ldo_channel_config_t ldo_config = {
.chan_id = config->ldo_channel,
.voltage_mv = config->ldo_voltage_mv,
.flags = {},
};
if (esp_ldo_acquire_channel(&ldo_config, &internal->ldo_handle) != ESP_OK) {
LOG_E(TAG, "Failed to acquire LDO channel for MIPI DSI PHY");
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
const esp_lcd_dsi_bus_config_t bus_config = {
.bus_id = config->dsi_bus_id,
.num_data_lanes = config->num_data_lanes,
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT,
.lane_bit_rate_mbps = config->lane_bit_rate_mbps,
};
if (esp_lcd_new_dsi_bus(&bus_config, &internal->dsi_bus_handle) != ESP_OK) {
LOG_E(TAG, "Failed to create MIPI DSI bus");
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
const esp_lcd_dbi_io_config_t dbi_config = {
.virtual_channel = 0,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
};
if (esp_lcd_new_panel_io_dbi(internal->dsi_bus_handle, &dbi_config, &internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to create panel IO");
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
const lcd_color_format_t color_format = color_format_from_bits_per_pixel(config->bits_per_pixel);
const esp_lcd_dpi_panel_config_t dpi_config = {
.virtual_channel = 0,
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = config->dpi_clock_freq_mhz,
.pixel_format = (lcd_color_rgb_pixel_format_t)0, // deprecated field - in/out_color_format below take precedence
.in_color_format = color_format,
.out_color_format = color_format,
.num_fbs = config->num_fbs,
.video_timing = {
.h_size = config->horizontal_resolution,
.v_size = config->vertical_resolution,
.hsync_pulse_width = config->hsync_pulse_width,
.hsync_back_porch = config->hsync_back_porch,
.hsync_front_porch = config->hsync_front_porch,
.vsync_pulse_width = config->vsync_pulse_width,
.vsync_back_porch = config->vsync_back_porch,
.vsync_front_porch = config->vsync_front_porch,
},
.flags = {
.use_dma2d = config->use_dma2d,
.disable_lp = config->disable_lp,
},
};
st7121_vendor_config_t vendor_config = {
.init_cmds = init_cmds,
.init_cmds_size = init_cmds_size,
.mipi_config = {
.dsi_bus = internal->dsi_bus_handle,
.dpi_config = &dpi_config,
},
};
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = pin_or_unused(config->pin_reset),
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = config->bits_per_pixel,
// ST7121's reset line is fixed active-low in hardware.
.flags = { .reset_active_high = false },
.vendor_config = &vendor_config,
};
if (esp_lcd_new_panel_st7121(internal->io_handle, &panel_config, &internal->panel_handle) != ESP_OK) {
LOG_E(TAG, "Failed to create panel");
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
// Bring-up sequence: reset() pulses (or software-resets) the panel and init() pushes
// init_cmds over the DBI command interface before bringing up the underlying DPI peripheral.
// swap_xy/set_gap are intentionally not called: the ST7121 driver doesn't override them and
// the underlying raw DPI panel doesn't implement them either, so both would just fail with
// ESP_ERR_NOT_SUPPORTED (see DisplayApi below). Every failure path here must clean up fully:
// unlike stop_device, this is never retried by the kernel if start_device fails (see
// device_start() in TactilityKernel), so a partial failure here would leak.
bool ok =
esp_lcd_panel_reset(internal->panel_handle) == ESP_OK &&
esp_lcd_panel_init(internal->panel_handle) == ESP_OK &&
esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK &&
esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK &&
esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
if (!ok) {
LOG_E(TAG, "Failed to bring up panel");
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
internal->frame_buffer_count = 0;
internal->frame_buffer_size_bytes = (size_t)config->horizontal_resolution * config->vertical_resolution *
((config->bits_per_pixel + 7) / 8);
if (config->num_fbs > 0) {
// esp_lcd_dpi_panel_get_frame_buffer() is variadic: the number of out-pointer arguments
// passed must match fb_num exactly, so this can't be a loop.
size_t fb_num = config->num_fbs < MAX_CACHED_FRAME_BUFFERS ? config->num_fbs : MAX_CACHED_FRAME_BUFFERS;
esp_err_t ret;
switch (fb_num) {
case 1:
ret = esp_lcd_dpi_panel_get_frame_buffer(internal->panel_handle, 1, &internal->frame_buffers[0]);
break;
case 2:
ret = esp_lcd_dpi_panel_get_frame_buffer(internal->panel_handle, 2, &internal->frame_buffers[0], &internal->frame_buffers[1]);
break;
default:
ret = ESP_OK;
break;
}
if (ret != ESP_OK) {
LOG_E(TAG, "Failed to get frame buffer(s): %s", esp_err_to_name(ret));
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
internal->frame_buffer_count = (uint8_t)fb_num;
}
internal->frame_complete_semaphore = xSemaphoreCreateBinary();
if (internal->frame_complete_semaphore == nullptr) {
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
internal->color_trans_done_semaphore = xSemaphoreCreateBinary();
if (internal->color_trans_done_semaphore == nullptr) {
vSemaphoreDelete(internal->frame_complete_semaphore);
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
// The panel starts out idle (no draw_bitmap() call in flight), so the first draw_bitmap()
// must not block waiting for a completion event that will never come.
xSemaphoreGive(internal->color_trans_done_semaphore);
esp_lcd_dpi_panel_event_callbacks_t callbacks = {};
callbacks.on_refresh_done = on_refresh_done;
callbacks.on_color_trans_done = on_color_trans_done;
if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) {
LOG_E(TAG, "Failed to register panel event callbacks");
vSemaphoreDelete(internal->color_trans_done_semaphore);
vSemaphoreDelete(internal->frame_complete_semaphore);
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
esp_lcd_del_dsi_bus(internal->dsi_bus_handle);
esp_ldo_release_channel(internal->ldo_handle);
free(internal->parsed_init_cmds);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
if (internal->panel_handle != nullptr) {
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel");
return ERROR_RESOURCE;
}
internal->panel_handle = nullptr;
}
if (internal->io_handle != nullptr) {
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel IO");
return ERROR_RESOURCE;
}
internal->io_handle = nullptr;
}
if (internal->dsi_bus_handle != nullptr) {
if (esp_lcd_del_dsi_bus(internal->dsi_bus_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete DSI bus");
return ERROR_RESOURCE;
}
internal->dsi_bus_handle = nullptr;
}
if (internal->ldo_handle != nullptr) {
if (esp_ldo_release_channel(internal->ldo_handle) != ESP_OK) {
LOG_E(TAG, "Failed to release LDO channel");
return ERROR_RESOURCE;
}
internal->ldo_handle = nullptr;
}
vSemaphoreDelete(internal->frame_complete_semaphore);
vSemaphoreDelete(internal->color_trans_done_semaphore);
free(internal->parsed_init_cmds);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// region DisplayApi
static error_t st7121_reset(Device* device) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7121_init(Device* device) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// Only block for scan-out completion when color_data is actually one of the panel's own frame
// buffers (see on_refresh_done's comment above for why that matters) and allow_tearing is not
// set - i.e. this specific call is a zero-copy flip, not a plain CPU copy into the panel's buffer
// from a caller-owned one (e.g. LVGL bound in owned-buffer mode), which has no reuse race to
// guard against and shouldn't pay the up-to-one-frame latency cost for every partial update.
static bool st7121_color_data_is_frame_buffer(const St7121Internal* internal, const void* color_data) {
const auto* ptr = static_cast<const uint8_t*>(color_data);
for (uint8_t i = 0; i < internal->frame_buffer_count; i++) {
const auto* base = static_cast<const uint8_t*>(internal->frame_buffers[i]);
if (ptr >= base && ptr < base + internal->frame_buffer_size_bytes) {
return true;
}
}
return false;
}
static error_t st7121_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<St7121Internal*>(device_get_driver_data(device));
// Wait for the previous draw_bitmap()'s copy/DMA2D transfer to finish before issuing a new
// one - see on_color_trans_done's comment for why this is unconditional (unlike the
// tearing-avoidance wait below). The semaphore starts pre-given (see start()), so the first
// call doesn't block here.
xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY);
bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && st7121_color_data_is_frame_buffer(internal, color_data);
if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal
}
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
xSemaphoreGive(internal->color_trans_done_semaphore);
return ERROR_RESOURCE;
}
// Also wait for *this* call's own copy to finish before returning: color_data may be a
// caller-owned buffer (e.g. LVGL's draw buffer in owned-buffer/PARTIAL mode), and the
// CPU-memcpy/DMA2D copy paths both read directly from it asynchronously - draw_bitmap()
// returning early would let the caller start overwriting color_data (DisplayApi's contract
// treats draw_bitmap as synchronous - see lvgl_display_flush_cb()'s lv_display_flush_ready()
// call right after) while that read is still in flight. on_color_trans_done() re-gives the
// semaphore here, restoring the pre-given/idle state for the next call.
xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY);
xSemaphoreGive(internal->color_trans_done_semaphore);
if (wait_for_scanout) {
xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY);
}
return ERROR_NONE;
}
// Mirror is always implemented by LCD command (MADCTL), unconditionally, by esp_lcd_st7121 - see
// panel_st7121_mirror() in esp_lcd_st7121.c. Unlike esp_lcd_rgb_panel's software rotate_mask
// trick, this isn't tied to draw_bitmap's copy path, so it stays available even when LVGL is
// bound directly onto the panel's own frame buffers.
static error_t st7121_mirror(Device* device, bool x_axis, bool y_axis) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static bool st7121_get_mirror_x(Device* device) {
return GET_CONFIG(device)->mirror_x;
}
static bool st7121_get_mirror_y(Device* device) {
return GET_CONFIG(device)->mirror_y;
}
// swap_xy/set_gap are not exposed: esp_lcd_st7121 doesn't override them and the underlying raw
// MIPI DPI panel doesn't implement them either (esp_lcd_panel_dpi.c never assigns those function
// pointers), so esp_lcd_panel_swap_xy()/set_gap() would just return ESP_ERR_NOT_SUPPORTED.
static error_t st7121_invert_color(Device* device, bool invert_color_data) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7121_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// disp_sleep is not exposed: esp_lcd_st7121 doesn't override it either (only
// del/init/reset/mirror/invert_color/disp_on_off - see esp_lcd_st7121.c).
// bgr_order only selects the panel controller's rgb_ele_order (applied in start(), above) so the
// R/B swap happens on-chip. LVGL always fills the same-layout buffer either way - there's no
// separate "BGR" memory layout to produce, unlike the SPI byte-order swap some other panels need.
static enum DisplayColorFormat st7121_get_color_format(Device* device) {
return GET_CONFIG(device)->bits_per_pixel == 24 ? DISPLAY_COLOR_FORMAT_RGB888 : DISPLAY_COLOR_FORMAT_RGB565;
}
static uint16_t st7121_get_resolution_x(Device* device) {
return GET_CONFIG(device)->horizontal_resolution;
}
static uint16_t st7121_get_resolution_y(Device* device) {
return GET_CONFIG(device)->vertical_resolution;
}
static void st7121_get_frame_buffer(Device* device, uint8_t index, void** out_buffer) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
*out_buffer = index < internal->frame_buffer_count ? internal->frame_buffers[index] : nullptr;
}
static uint8_t st7121_get_frame_buffer_count(Device* device) {
auto* internal = static_cast<St7121Internal*>(device_get_driver_data(device));
return internal->frame_buffer_count;
}
static error_t st7121_get_backlight(Device* device, Device** backlight) {
auto* configured_backlight = GET_CONFIG(device)->backlight;
if (configured_backlight == nullptr) {
return ERROR_NOT_SUPPORTED;
}
*backlight = configured_backlight;
return ERROR_NONE;
}
// endregion
static const DisplayApi st7121_display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT,
.reset = st7121_reset,
.init = st7121_init,
.draw_bitmap = st7121_draw_bitmap,
.mirror = st7121_mirror,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = st7121_get_mirror_x,
.get_mirror_y = st7121_get_mirror_y,
.set_gap = nullptr,
.invert_color = st7121_invert_color,
.disp_on_off = st7121_disp_on_off,
.disp_sleep = nullptr,
.get_color_format = st7121_get_color_format,
.get_resolution_x = st7121_get_resolution_x,
.get_resolution_y = st7121_get_resolution_y,
.get_frame_buffer = st7121_get_frame_buffer,
.get_frame_buffer_count = st7121_get_frame_buffer_count,
.get_backlight = st7121_get_backlight,
.has_capability = nullptr,
};
Driver st7121_driver = {
.name = "st7121",
.compatible = (const char*[]) { "sitronix,st7121", nullptr },
.start_device = start,
.stop_device = stop,
.api = &st7121_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &st7121_module,
.internal = nullptr
};
#endif // SOC_MIPI_DSI_SUPPORTED

View File

@ -100,6 +100,7 @@ else ()
gps-module
gps-generic-module
gps-meshtastic-module
service-module
SDL2::SDL2-static
SDL2-static
)

View File

@ -58,6 +58,11 @@ dependencies:
rules:
# More hardware seems to be supported - enable as needed
- if: "target in [esp32p4]"
espressif/esp_lcd_st7121:
version: "1.0.1"
rules:
# More hardware seems to be supported - enable as needed
- if: "target in [esp32p4]"
espressif/esp_lcd_touch_st7123:
version: "1.0.1"
rules:

View File

@ -8,5 +8,5 @@ tactility_add_module(gps-module
SRCS ${SOURCE_FILES}
PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/
REQUIRES TactilityKernel minmea
REQUIRES TactilityKernel service-module minmea
)

View File

@ -2,9 +2,10 @@
#include <gps/gps_settings.h>
#include <gps/private/gps_ledger.h>
#include <service/paths.h>
#include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h>
#include <tactility/service/service_paths.h>
#include <sys/stat.h>

View File

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(service-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,2 @@
dependencies:
- TactilityKernel

View File

@ -2,8 +2,9 @@
#pragma once
#include <service/manifest.h>
#include <tactility/error.h>
#include <tactility/service/service_manifest.h>
#ifdef __cplusplus
extern "C" {

View File

@ -3,9 +3,9 @@
#pragma once
#include <stdbool.h>
#include <service/instance.h>
#include <service/manifest.h>
#include <tactility/error.h>
#include <tactility/service/service_instance.h>
#include <tactility/service/service_manifest.h>
#ifdef __cplusplus
extern "C" {

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_instance.h>
#include <service/instance.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>

View File

@ -1,11 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_manager.h>
#include "tactility/system_event.h"
#include <service/manager.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
#include <tactility/system_event.h>
#include <new>
#include <string>

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_paths.h>
#include <service/paths.h>
#include <tactility/paths.h>
#include <cstdio>

View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/module.h>
#include <service/instance.h>
#include <service/manager.h>
#include <service/paths.h>
const ModuleSymbol service_module_symbols[] = {
// service/service_instance
DEFINE_MODULE_SYMBOL(service_instance_construct),
DEFINE_MODULE_SYMBOL(service_instance_destruct),
DEFINE_MODULE_SYMBOL(service_instance_get_manifest),
DEFINE_MODULE_SYMBOL(service_instance_get_data),
DEFINE_MODULE_SYMBOL(service_instance_get_state),
// service/service_manager
DEFINE_MODULE_SYMBOL(service_manager_add),
DEFINE_MODULE_SYMBOL(service_manager_remove),
DEFINE_MODULE_SYMBOL(service_manager_find_manifest),
DEFINE_MODULE_SYMBOL(service_manager_start),
DEFINE_MODULE_SYMBOL(service_manager_stop),
DEFINE_MODULE_SYMBOL(service_manager_get_state),
DEFINE_MODULE_SYMBOL(service_manager_find_instance),
// service/service_paths
DEFINE_MODULE_SYMBOL(service_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(service_paths_get_user_data_path),
DEFINE_MODULE_SYMBOL(service_paths_get_assets_directory),
DEFINE_MODULE_SYMBOL(service_paths_get_assets_path),
// terminator
MODULE_SYMBOL_TERMINATOR
};
Module service_module = {
.name = "service",
.drivers = nullptr,
.symbols = service_module_symbols,
.internal = nullptr
};

View File

@ -12,6 +12,7 @@ list(APPEND REQUIRES_LIST
gps-module
gps-generic-module
gps-meshtastic-module
service-module
lv_screenshot
minitar
)

View File

@ -1,6 +1,6 @@
#pragma once
#include <tactility/service/service_instance.h>
#include <service/instance.h>
#include <memory>

View File

@ -1,7 +1,7 @@
#pragma once
#include <memory>
#include <tactility/service/service_manifest.h>
#include <service/manifest.h>
struct ServiceInstance;

View File

@ -2,8 +2,9 @@
#include <tactility/check.h>
#include <tactility/error.h>
#include <tactility/service/service_manifest.h>
#include <tactility/service/service_paths.h>
#include <service/manifest.h>
#include <service/paths.h>
#include <cassert>
#include <string>

View File

@ -1,8 +1,7 @@
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServicePaths.h>
#include <tactility/service/service_instance.h>
#include <service/instance.h>
namespace tt::service {

View File

@ -1,12 +1,12 @@
#include <Tactility/service/ServiceRegistration.h>
#include <service/manager.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceManifest.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/service/service_manager.h>
#include <cassert>
#include <memory>

View File

@ -64,7 +64,6 @@ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
*/
error_t keyboard_get_backlight(struct Device* device, struct Device** backlight_device);
extern const struct DeviceType KEYBOARD_TYPE;
#ifdef __cplusplus

View File

@ -41,9 +41,6 @@
#include <tactility/filesystem/file_system.h>
#include <tactility/memory.h>
#include <tactility/module.h>
#include <tactility/service/service_instance.h>
#include <tactility/service/service_manager.h>
#include <tactility/service/service_paths.h>
#include <tactility/wifi_auto_scan.h>
#ifndef ESP_PLATFORM
@ -473,25 +470,6 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(module_is_started),
DEFINE_MODULE_SYMBOL(module_resolve_symbol),
DEFINE_MODULE_SYMBOL(module_resolve_symbol_global),
// service/service_instance
DEFINE_MODULE_SYMBOL(service_instance_construct),
DEFINE_MODULE_SYMBOL(service_instance_destruct),
DEFINE_MODULE_SYMBOL(service_instance_get_manifest),
DEFINE_MODULE_SYMBOL(service_instance_get_data),
DEFINE_MODULE_SYMBOL(service_instance_get_state),
// service/service_manager
DEFINE_MODULE_SYMBOL(service_manager_add),
DEFINE_MODULE_SYMBOL(service_manager_remove),
DEFINE_MODULE_SYMBOL(service_manager_find_manifest),
DEFINE_MODULE_SYMBOL(service_manager_start),
DEFINE_MODULE_SYMBOL(service_manager_stop),
DEFINE_MODULE_SYMBOL(service_manager_get_state),
DEFINE_MODULE_SYMBOL(service_manager_find_instance),
// service/service_paths
DEFINE_MODULE_SYMBOL(service_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(service_paths_get_user_data_path),
DEFINE_MODULE_SYMBOL(service_paths_get_assets_directory),
DEFINE_MODULE_SYMBOL(service_paths_get_assets_path),
// terminator
MODULE_SYMBOL_TERMINATOR
};

View File

@ -3,12 +3,14 @@ project(tests)
set(DOCTESTINC ${PROJECT_SOURCE_DIR}/Doctest/Include)
enable_testing()
add_subdirectory(service-module)
add_subdirectory(TactilityFreeRtos)
add_subdirectory(TactilityKernel)
add_subdirectory(Tactility)
add_subdirectory(crypt-module)
add_custom_target(build-tests)
add_dependencies(build-tests ServiceModuleTests)
add_dependencies(build-tests TactilityFreeRtosTests)
add_dependencies(build-tests TactilityTests)
add_dependencies(build-tests TactilityKernelTests)

View File

@ -19,6 +19,7 @@ target_link_libraries(TactilityTests PRIVATE
gps-module
gps-generic-module
gps-meshtastic-module
service-module
lvgl
SDL2::SDL2-static SDL2-static
)

View File

@ -13,4 +13,5 @@ add_test(NAME TactilityKernelTests COMMAND TactilityKernelTests)
target_link_libraries(TactilityKernelTests PUBLIC
TactilityKernel
platform-posix
service-module
)

View File

@ -0,0 +1,18 @@
project(ServiceModuleTests)
enable_language(C CXX ASM)
file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
add_executable(ServiceModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
target_include_directories(ServiceModuleTests PRIVATE ${DOCTESTINC})
add_test(NAME ServiceModuleTests COMMAND ServiceModuleTests)
target_link_libraries(ServiceModuleTests PUBLIC
TactilityKernel
service-module
platform-posix
freertos_kernel
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,51 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cassert>
#include "FreeRTOS.h"
#include "task.h"
typedef struct {
int argc;
char** argv;
int result;
} TestTaskData;
void test_task(void* parameter) {
auto* data = (TestTaskData*)parameter;
doctest::Context context;
context.applyCommandLine(data->argc, data->argv);
// overrides
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
data->result = context.run();
vTaskEndScheduler();
vTaskDelete(nullptr);
}
int main(int argc, char** argv) {
TestTaskData data = {
.argc = argc,
.argv = argv,
.result = 0
};
BaseType_t task_result = xTaskCreate(
test_task,
"test_task",
8192,
&data,
1,
nullptr
);
assert(task_result == pdPASS);
vTaskStartScheduler();
return data.result;
}

View File

@ -1,6 +1,8 @@
#include "doctest.h"
#include <service/paths.h>
#include <tactility/paths.h>
#include <tactility/service/service_paths.h>
#include <cstring>
#include <string>

View File

@ -1,5 +1,6 @@
#include "doctest.h"
#include <tactility/service/service_manager.h>
#include <service/manager.h>
// Defined in service_instance.cpp. Internal-only, exposed here to test try_get/put gating.
extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state);