Compare commits

..

2 Commits

Author SHA1 Message Date
Adolfo Reyna cd99a8d7c1 feat(app): allow external apps to hide statusbar via manifest flags
- Add parseAppFlagsString() in AppManifestParsing.cpp to parse comma-separated flags (HideStatusBar, Hidden, None)
- Parse [app]flags in V1 and app.flags in V2 manifests
- Loader already supports HideStatusBar, GuiService hides statusbarWidget when set
- Enables fullscreen for ELF apps (e.g. BibleVerse, BookPlayer) without firmware recompilation of internal manifests
- Add tests for flag parsing (38 tests passing)
- Tested on es3c28p /dev/cu.usbmodem1101 @ 192.168.68.133 with HelloWorld app
2026-07-21 11:15:15 -04:00
Adolfo Reyna 2720556d6a fix(audio): ES8311 BOTH complementary open + audio-stream close ref-count + mic unmute
- es8311 driver open() now allows OUTPUT->INPUT complementary (promotes to BOTH) when same native rate 44100
- audio-stream close_stream() ref-counts shared BOTH codec (don't close if other direction still open)
- MCP recordVoice + VoiceRecorder app explicitly unmute and set 100% gain

Fixes mic input not working - was returning ERROR_RESOURCE when output already open
2026-07-21 11:15:15 -04:00
20 changed files with 145 additions and 776 deletions
@@ -1,8 +0,0 @@
# Force CMake reload to detect new files module.cpp and Configuration.cpp
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility ButtonControl ST7305 PwmBacklight driver
)
@@ -1,41 +0,0 @@
#include "devices/Display.h"
#include <Tactility/hal/Configuration.h>
#include <ButtonControl.h>
#include <driver/gpio.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static constexpr auto* TAG = "WaveshareRLCD";
using namespace tt::hal;
static DeviceVector createDevices() {
return {
createDisplay(),
ButtonControl::createTwoButtonControl(GPIO_NUM_18, GPIO_NUM_0) // KEY=18 cycles, BOOT=0 selects
};
}
static bool initBoot() {
// Amp enable GPIO46 active HIGH - keep LOW during boot to avoid pop,
// then HIGH after delay so speaker works for MCP / Mp3Player
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_46);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
gpio_set_level(GPIO_NUM_46, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_NUM_46, 1);
LOG_I(TAG, "Speaker amp GPIO46 enabled");
return true;
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
@@ -1,23 +0,0 @@
#include "Display.h"
#include <St7305Display.h>
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto configuration = std::make_unique<St7305Display::Configuration>(
SPI2_HOST,
GPIO_NUM_40, // CS
GPIO_NUM_5, // DC
400, // Width
300, // Height
nullptr, // Touch device
false, // swapXY
false, // mirrorX
false, // mirrorY
true, // invertColor - initial true to match existing inverted look, user can toggle via Settings->Display
0 // bufferSize (0 = default, which is full screen = 120,000 pixels)
);
configuration->resetPin = GPIO_NUM_41;
auto display = std::make_shared<St7305Display>(std::move(configuration));
return std::reinterpret_pointer_cast<tt::hal::display::DisplayDevice>(display);
}
@@ -1,6 +0,0 @@
#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <memory>
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
@@ -1,21 +0,0 @@
#include <tactility/module.h>
extern "C" {
static error_t start() {
return ERROR_NONE;
}
static error_t stop() {
return ERROR_NONE;
}
struct Module waveshare_esp32_s3_rlcd_module = {
.name = "waveshare-esp32-s3-rlcd",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}
@@ -1,26 +0,0 @@
general.vendor=WaveShare
general.name=ESP32-S3-RLCD-4.2
general.incubating=true
apps.launcherAppId=Launcher
# apps.autoStartAppId=ApWebServer
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
storage.userDataLocation=SD
display.size=4.2"
display.shape=rectangle
display.dpi=120
lvgl.colorDepth=16
lvgl.uiDensity=compact
lvgl.fontSize=20
lvgl.theme=DefaultDark
@@ -1,3 +0,0 @@
dependencies:
- Platforms/platform-esp32
dts: waveshare,esp32-s3-rlcd.dts
@@ -1,78 +0,0 @@
/dts-v1/;
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/display_placeholder.h>
/ {
compatible = "root";
model = "Waveshare ESP32-S3-RLCD-4.2";
ble0 {
compatible = "espressif,esp32-ble";
status = "disabled";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
// Audio: ES7210 mic + speaker via I2S0
// Pins verified from working MicroPython board_config.py for WAVESHARE_RLCD:
// mclk=16, bclk=9, ws=45, tx=8 (DAC out), rx=10 (ES7210 mic in)
// NOTE: Display DC is GPIO5, so I2S must NOT use GPIO5 to avoid bus corruption
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 9 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 45 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 10 GPIO_FLAG_NONE>;
pin-mclk = <&gpio0 16 GPIO_FLAG_NONE>;
};
i2c0 {
compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
pin-sda = <&gpio0 13 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 14 GPIO_FLAG_NONE>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
cs-gpios = <&gpio0 40 GPIO_FLAG_NONE>;
pin-mosi = <&gpio0 12 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>;
display {
compatible = "display-placeholder";
};
};
sdmmc0 {
compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 38 GPIO_FLAG_NONE>;
pin-cmd = <&gpio0 21 GPIO_FLAG_NONE>;
pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 17 GPIO_FLAG_NONE>;
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <1>;
pullups;
};
uart0 {
compatible = "espressif,esp32-uart";
port = <UART_NUM_0>;
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
};
};
-5
View File
@@ -1,5 +0,0 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility driver EspLcdCompat
)
-108
View File
@@ -1,108 +0,0 @@
#include "St7305Display.h"
#include "esp_lcd_st7305.h"
#include <tactility/log.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lvgl_port.h>
static const char* TAG = "ST7305";
bool St7305Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
LOG_I(TAG, "Starting ST7305 SPI panel IO creation");
const esp_lcd_panel_io_spi_config_t panel_io_config = {
.cs_gpio_num = configuration->csPin,
.dc_gpio_num = configuration->dcPin,
.spi_mode = 0,
.pclk_hz = configuration->pixelClockFrequency,
.trans_queue_depth = configuration->transactionQueueDepth,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 0,
.sio_mode = 0,
.lsb_first = 0,
.cs_high_active = 0
}
};
if (esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &outHandle) != ESP_OK) {
LOG_E(TAG, "Failed to create panel SPI IO");
return false;
}
return true;
}
bool St7305Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) {
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = configuration->resetPin,
.color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
.bits_per_pixel = 1,
.flags = {
.reset_active_high = false
},
.vendor_config = nullptr
};
if (esp_lcd_new_panel_st7305(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to create st7305 panel");
return false;
}
// ST7305 needs extra delay after reset before init — prevents freeze on fast boot
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to reset st7305 panel");
return false;
}
vTaskDelay(pdMS_TO_TICKS(150));
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
LOG_E(TAG, "Failed to init st7305 panel");
return false;
}
vTaskDelay(pdMS_TO_TICKS(50));
if (configuration->invertColor) {
if (esp_lcd_panel_invert_color(panelHandle, true) != ESP_OK) {
LOG_W(TAG, "Failed to apply initial invertColor");
}
}
return true;
}
lvgl_port_display_cfg_t St7305Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) {
return lvgl_port_display_cfg_t {
.io_handle = ioHandle,
.panel_handle = panelHandle,
.control_handle = nullptr,
.buffer_size = configuration->bufferSize,
.double_buffer = false, // Monochrome displays usually use single buffering to save RAM
.trans_size = 0,
.hres = configuration->horizontalResolution,
.vres = configuration->verticalResolution,
.monochrome = true, // Enables esp_lvgl_port monochrome converter
.rotation = {
.swap_xy = false,
.mirror_x = true,
.mirror_y = false,
},
.color_format = LV_COLOR_FORMAT_RGB565, // Must be RGB565 for monochrome mode to trigger converter
.flags = {
.buff_dma = false,
.buff_spiram = false,
.sw_rotate = false,
.swap_bytes = false,
.full_refresh = true, // We want full refresh to rewrite the converted block format to ST7305
.direct_mode = false
}
};
}
-100
View File
@@ -1,100 +0,0 @@
#pragma once
#include <EspLcdDisplay.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <driver/gpio.h>
#include <driver/spi_common.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_types.h>
#include <functional>
#include <lvgl.h>
class St7305Display final : public EspLcdDisplay {
public:
class Configuration {
public:
Configuration(
spi_host_device_t spiHostDevice,
gpio_num_t csPin,
gpio_num_t dcPin,
unsigned int horizontalResolution,
unsigned int verticalResolution,
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
bool swapXY = false,
bool mirrorX = false,
bool mirrorY = false,
bool invertColor = false,
uint32_t bufferSize = 0
) : spiHostDevice(spiHostDevice),
csPin(csPin),
dcPin(dcPin),
horizontalResolution(horizontalResolution),
verticalResolution(verticalResolution),
swapXY(swapXY),
mirrorX(mirrorX),
mirrorY(mirrorY),
invertColor(invertColor),
bufferSize(bufferSize),
touch(std::move(touch))
{
if (this->bufferSize == 0) {
// For monochrome display, full pixel count is expected for buffer size
this->bufferSize = horizontalResolution * verticalResolution;
}
}
spi_host_device_t spiHostDevice;
gpio_num_t csPin;
gpio_num_t dcPin;
gpio_num_t resetPin = GPIO_NUM_NC;
unsigned int pixelClockFrequency = 10'000'000; // 10MHz SPI clock for ST7305
size_t transactionQueueDepth = 10;
unsigned int horizontalResolution;
unsigned int verticalResolution;
bool swapXY = false;
bool mirrorX = false;
bool mirrorY = false;
bool invertColor = false;
uint32_t bufferSize = 0;
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
std::function<void(uint8_t)> _Nullable backlightDutyFunction = nullptr;
};
private:
std::unique_ptr<Configuration> configuration;
bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override;
lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override;
public:
explicit St7305Display(std::unique_ptr<Configuration> inConfiguration) :
configuration(std::move(inConfiguration))
{}
std::string getName() const override { return "ST7305"; }
std::string getDescription() const override { return "ST7305 monochrome reflective LCD display"; }
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override { return configuration->touch; }
void setBacklightDuty(uint8_t backlightDuty) override {
if (configuration->backlightDutyFunction != nullptr) {
configuration->backlightDutyFunction(backlightDuty);
}
}
bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; }
};
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
-300
View File
@@ -1,300 +0,0 @@
#include "esp_lcd_st7305.h"
#include "soc/soc_caps.h"
#include "esp_check.h"
#include "esp_lcd_types.h"
#include <stdlib.h>
#include <sys/cdefs.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_interface.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_commands.h"
#include "driver/gpio.h"
#include <string.h>
#include "esp_log.h"
static const char *TAG = "st7305";
static esp_err_t panel_st7305_del(esp_lcd_panel_t *panel);
static esp_err_t panel_st7305_reset(esp_lcd_panel_t *panel);
static esp_err_t panel_st7305_init(esp_lcd_panel_t *panel);
static esp_err_t panel_st7305_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
static esp_err_t panel_st7305_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
static esp_err_t panel_st7305_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
static esp_err_t panel_st7305_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
static esp_err_t panel_st7305_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
static esp_err_t panel_st7305_disp_on_off(esp_lcd_panel_t *panel, bool off);
typedef struct {
esp_lcd_panel_t base;
esp_lcd_panel_io_handle_t io;
int reset_gpio_num;
bool reset_level;
int x_gap;
int y_gap;
int width;
int height;
uint8_t rotation;
uint8_t madctl_val;
const st7305_lcd_init_cmd_t *init_cmds;
uint16_t init_cmds_size;
uint8_t *draw_buffer;
} st7305_panel_t;
static const st7305_lcd_init_cmd_t st7305_init_cmds[] = {
{0xD6, (uint8_t[]){0x17, 0x02}, 2, 0}, // NVM Load Control
{0xD1, (uint8_t[]){0x01}, 1, 0}, // Booster Enable
{0xC0, (uint8_t[]){0x11, 0x04}, 2, 0}, // Gate Voltage Setting
{0xC1, (uint8_t[]){0x41, 0x41, 0x41, 0x41}, 4, 0}, // VSHP Setting
{0xC2, (uint8_t[]){0x19, 0x19, 0x19, 0x19}, 4, 0}, // VSLP Setting
{0xC4, (uint8_t[]){0x41, 0x41, 0x41, 0x41}, 4, 0}, // VSHN Setting
{0xC5, (uint8_t[]){0x19, 0x13, 0x19, 0x19}, 4, 0}, // VSLN Setting
{0xD8, (uint8_t[]){0xA6, 0xE9}, 2, 0}, // OSC Setting
{0xB2, (uint8_t[]){0x05}, 1, 0}, // Frame Rate Control
{0xB3, (uint8_t[]){0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45}, 10, 0}, // Gate EQ HPM
{0xB4, (uint8_t[]){0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45}, 8, 0}, // Gate EQ LPM
{0x62, (uint8_t[]){0x32, 0x03, 0x1F}, 3, 0}, // Gate Timing Control
{0xB7, (uint8_t[]){0x13}, 1, 0}, // Source EQ Enable
{0xB0, (uint8_t[]){0x64}, 1, 0}, // Gate Line Setting: 300 line (0x64 = 100 * 3)
{0x11, NULL, 0, 200}, // Sleep out
{0xC9, (uint8_t[]){0x00}, 1, 0}, // Source Voltage Select
{0x36, (uint8_t[]){0x48}, 1, 0}, // Memory Data Access Control (MX=1, DO=1)
{0x3A, (uint8_t[]){0x11}, 1, 0}, // Data Format Select: 1bpp
{0xB9, (uint8_t[]){0x20}, 1, 0}, // Gamma Mode Setting
{0xB8, (uint8_t[]){0x29}, 1, 0}, // Panel Setting
{0x21, NULL, 0, 0}, // Display Inversion On
{0x2A, (uint8_t[]){0x12, 0x2A}, 2, 0}, // Column Address Setting
{0x2B, (uint8_t[]){0x00, 0xC7}, 2, 0}, // Row Address Setting
{0x35, (uint8_t[]){0x00}, 1, 0}, // TE Line
{0xD0, (uint8_t[]){0xFF}, 1, 0}, // Auto power down ON
{0x38, NULL, 0, 0}, // High Power Mode ON
{0x29, NULL, 0, 100}, // Display ON
};
esp_err_t esp_lcd_new_panel_st7305(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
esp_lcd_panel_handle_t *ret_panel)
{
esp_err_t ret = ESP_OK;
st7305_panel_t *st7305 = NULL;
gpio_config_t io_conf = { 0 };
ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
st7305 = (st7305_panel_t *)calloc(1, sizeof(st7305_panel_t));
ESP_GOTO_ON_FALSE(st7305, ESP_ERR_NO_MEM, err, TAG, "no mem for st7305 panel");
if (panel_dev_config->reset_gpio_num >= 0) {
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num;
ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
}
st7305->width = ST7305_WIDTH;
st7305->height = ST7305_HEIGHT;
st7305->madctl_val = 0x48; // MX=1, DO=1
st7305->rotation = 0;
st7305->io = io;
st7305->reset_gpio_num = panel_dev_config->reset_gpio_num;
st7305->reset_level = panel_dev_config->flags.reset_active_high;
if (panel_dev_config->vendor_config) {
st7305->init_cmds = ((st7305_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds;
st7305->init_cmds_size = ((st7305_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size;
} else {
st7305->init_cmds = st7305_init_cmds;
st7305->init_cmds_size = sizeof(st7305_init_cmds) / sizeof(st7305_lcd_init_cmd_t);
}
st7305->draw_buffer = heap_caps_malloc(15000, MALLOC_CAP_DMA);
ESP_GOTO_ON_FALSE(st7305->draw_buffer, ESP_ERR_NO_MEM, err, TAG, "no mem for st7305 draw buffer");
memset(st7305->draw_buffer, 0, 15000);
st7305->base.del = panel_st7305_del;
st7305->base.reset = panel_st7305_reset;
st7305->base.init = panel_st7305_init;
st7305->base.draw_bitmap = panel_st7305_draw_bitmap;
st7305->base.invert_color = panel_st7305_invert_color;
st7305->base.set_gap = panel_st7305_set_gap;
st7305->base.mirror = panel_st7305_mirror;
st7305->base.swap_xy = panel_st7305_swap_xy;
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
st7305->base.disp_off = panel_st7305_disp_on_off;
#else
st7305->base.disp_on_off = panel_st7305_disp_on_off;
#endif
*ret_panel = &(st7305->base);
ESP_LOGD(TAG, "new st7305 panel @%p", st7305);
return ESP_OK;
err:
if (st7305) {
if (panel_dev_config->reset_gpio_num >= 0) {
gpio_reset_pin(panel_dev_config->reset_gpio_num);
}
if (st7305->draw_buffer) {
free(st7305->draw_buffer);
}
free(st7305);
}
return ret;
}
static esp_err_t panel_st7305_del(esp_lcd_panel_t *panel)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
if (st7305->reset_gpio_num >= 0) {
gpio_reset_pin(st7305->reset_gpio_num);
}
if (st7305->draw_buffer) {
free(st7305->draw_buffer);
}
free(st7305);
return ESP_OK;
}
static esp_err_t panel_st7305_reset(esp_lcd_panel_t *panel)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
if (st7305->reset_gpio_num >= 0) {
gpio_set_level(st7305->reset_gpio_num, !st7305->reset_level);
vTaskDelay(pdMS_TO_TICKS(50));
gpio_set_level(st7305->reset_gpio_num, st7305->reset_level);
vTaskDelay(pdMS_TO_TICKS(20));
gpio_set_level(st7305->reset_gpio_num, !st7305->reset_level);
vTaskDelay(pdMS_TO_TICKS(50));
}
return ESP_OK;
}
static esp_err_t panel_st7305_init(esp_lcd_panel_t *panel)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
esp_lcd_panel_io_handle_t io = st7305->io;
for (size_t i = 0; i < st7305->init_cmds_size; i++) {
if (st7305->init_cmds[i].data_bytes > 0) {
esp_lcd_panel_io_tx_param(io, st7305->init_cmds[i].cmd,
st7305->init_cmds[i].data,
st7305->init_cmds[i].data_bytes);
} else {
esp_lcd_panel_io_tx_param(io, st7305->init_cmds[i].cmd, NULL, 0);
}
if (st7305->init_cmds[i].delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(st7305->init_cmds[i].delay_ms));
}
}
// Explicitly clear display RAM to white
if (st7305->draw_buffer) {
memset(st7305->draw_buffer, 0xFF, 15000); // 0xFF is White
uint8_t caset[] = {0x12, 0x2A};
uint8_t raset[] = {0x00, 0xC7};
esp_lcd_panel_io_tx_param(io, ST7305_CMD_CASET, caset, sizeof(caset));
esp_lcd_panel_io_tx_param(io, ST7305_CMD_RASET, raset, sizeof(raset));
esp_lcd_panel_io_tx_color(io, ST7305_CMD_RAMWR, st7305->draw_buffer, 15000);
vTaskDelay(pdMS_TO_TICKS(50));
}
return ESP_OK;
}
static esp_err_t panel_st7305_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
esp_lcd_panel_io_handle_t io = st7305->io;
const uint8_t *src = (const uint8_t *)color_data;
// Convert from vertical-page format (SSD1306-style) to ST7305 landscape 2x4 block format
for (int y = 0; y < 300; y++) {
int inv_y = 299 - y;
int block_y = inv_y >> 2;
int local_y = inv_y & 3;
int src_y_div_8 = y >> 3;
int src_y_mod_8 = y & 7;
uint8_t src_bit_mask = 1 << src_y_mod_8;
for (int x = 0; x < 400; x++) {
int byte_x = x >> 1;
int local_x = x & 1;
int dest_byte_idx = byte_x * 75 + block_y;
int dest_bit_pos = 7 - ((local_y << 1) | local_x);
int src_byte_idx = 400 * src_y_div_8 + x;
// Read standard vertical page byte bit
bool is_pixel_set = (src[src_byte_idx] & src_bit_mask) != 0;
// Invert the pixel logic if required, standard:
// esp_lvgl_port monochrome transform clears the bit (0) for light/chroma colors
// and sets the bit (1) for dark/black.
// In ST7305 display RAM, White/Light is 1, Black/Dark is 0.
// So we write: 1 (White) if is_pixel_set is false (light), and 0 (Black) if is_pixel_set is true (dark).
if (!is_pixel_set) {
st7305->draw_buffer[dest_byte_idx] |= (1 << dest_bit_pos);
} else {
st7305->draw_buffer[dest_byte_idx] &= ~(1 << dest_bit_pos);
}
}
}
uint8_t caset[] = {0x12, 0x2A};
uint8_t raset[] = {0x00, 0xC7};
esp_lcd_panel_io_tx_param(io, ST7305_CMD_CASET, caset, sizeof(caset));
esp_lcd_panel_io_tx_param(io, ST7305_CMD_RASET, raset, sizeof(raset));
esp_lcd_panel_io_tx_color(io, ST7305_CMD_RAMWR, st7305->draw_buffer, 15000);
return ESP_OK;
}
static esp_err_t panel_st7305_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
return esp_lcd_panel_io_tx_param(st7305->io, invert_color_data ? ST7305_CMD_INVON : ST7305_CMD_INVOFF, NULL, 0);
}
static esp_err_t panel_st7305_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
esp_lcd_panel_io_handle_t io = st7305->io;
if (mirror_x) {
st7305->madctl_val |= ST7305_MADCTL_MX;
} else {
st7305->madctl_val &= ~ST7305_MADCTL_MX;
}
if (mirror_y) {
st7305->madctl_val |= ST7305_MADCTL_MY;
} else {
st7305->madctl_val &= ~ST7305_MADCTL_MY;
}
return esp_lcd_panel_io_tx_param(io, ST7305_CMD_MADCTL, &st7305->madctl_val, 1);
}
static esp_err_t panel_st7305_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
esp_lcd_panel_io_handle_t io = st7305->io;
if (swap_axes) {
st7305->madctl_val |= ST7305_MADCTL_MV;
} else {
st7305->madctl_val &= ~ST7305_MADCTL_MV;
}
return esp_lcd_panel_io_tx_param(io, ST7305_CMD_MADCTL, (uint8_t[]) { st7305->madctl_val }, 1);
}
static esp_err_t panel_st7305_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
st7305->x_gap = x_gap;
st7305->y_gap = y_gap;
return ESP_OK;
}
static esp_err_t panel_st7305_disp_on_off(esp_lcd_panel_t *panel, bool off)
{
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
return esp_lcd_panel_io_tx_param(st7305->io, off ? ST7305_CMD_DISPOFF : ST7305_CMD_DISPON, NULL, 0);
}
-49
View File
@@ -1,49 +0,0 @@
#pragma once
#include <stdint.h>
#include "esp_lcd_types.h"
#include "esp_lcd_panel_vendor.h"
#include "sdkconfig.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
int cmd; /*<! The specific LCD command */
const void *data; /*<! Buffer that holds the command specific data */
size_t data_bytes; /*<! Size of `data` in memory, in bytes */
unsigned int delay_ms; /*<! Delay in milliseconds after this command */
} st7305_lcd_init_cmd_t;
typedef struct {
const st7305_lcd_init_cmd_t *init_cmds;
uint16_t init_cmds_size;
} st7305_vendor_config_t;
#define ST7305_WIDTH 400
#define ST7305_HEIGHT 300
#define ST7305_CMD_NOP 0x00
#define ST7305_CMD_SWRESET 0x01
#define ST7305_CMD_SLPOUT 0x11
#define ST7305_CMD_NORON 0x13
#define ST7305_CMD_INVOFF 0x20
#define ST7305_CMD_INVON 0x21
#define ST7305_CMD_DISPOFF 0x28
#define ST7305_CMD_DISPON 0x29
#define ST7305_CMD_CASET 0x2A
#define ST7305_CMD_RASET 0x2B
#define ST7305_CMD_RAMWR 0x2C
#define ST7305_CMD_MADCTL 0x36
#define ST7305_MADCTL_MY 0x80
#define ST7305_MADCTL_MX 0x40
#define ST7305_MADCTL_MV 0x20
#define ST7305_MADCTL_ML 0x10
esp_err_t esp_lcd_new_panel_st7305(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel);
#ifdef __cplusplus
}
#endif
@@ -553,9 +553,14 @@ error_t close_stream(AudioStreamHandle handle_base) {
Device* codec = is_input ? data->input_codec : data->output_codec; Device* codec = is_input ? data->input_codec : data->output_codec;
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output; AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
// Determine if underlying codec is shared (BOTH codec used for both directions)
// In that case we must NOT close the codec if the other direction is still active.
Device* other_codec = is_input ? data->output_codec : data->input_codec;
AudioStreamHandleImpl** other_slot = is_input ? &data->open_output : &data->open_input;
bool codec_shared = (codec != nullptr && other_codec != nullptr && codec == other_codec);
xSemaphoreTake(data->mutex, portMAX_DELAY); xSemaphoreTake(data->mutex, portMAX_DELAY);
if (handle->closing) { if (handle->closing) {
// Already being closed by another caller (e.g. concurrent set_enabled + app close).
xSemaphoreGive(data->mutex); xSemaphoreGive(data->mutex);
return ERROR_NONE; return ERROR_NONE;
} }
@@ -563,14 +568,16 @@ error_t close_stream(AudioStreamHandle handle_base) {
if (*slot == handle) { if (*slot == handle) {
*slot = nullptr; *slot = nullptr;
} }
bool other_still_open = (other_slot != nullptr && *other_slot != nullptr && *other_slot != reinterpret_cast<AudioStreamHandleImpl*>(1));
bool must_drain = (handle->busy_count > 0); bool must_drain = (handle->busy_count > 0);
bool should_close_codec = !codec_shared || !other_still_open;
xSemaphoreGive(data->mutex); xSemaphoreGive(data->mutex);
if (must_drain && handle->drain_semaphore != nullptr) { if (must_drain && handle->drain_semaphore != nullptr) {
xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY); xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY);
} }
if (codec != nullptr) { if (should_close_codec && codec != nullptr) {
audio_codec_close(codec); audio_codec_close(codec);
} }
+26 -6
View File
@@ -53,16 +53,36 @@ error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
}; };
if (data->is_open) { if (data->is_open) {
// open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the // ES8311 is configured for WORK_MODE_BOTH, so an already-open device
// same sample settings -- only an exact direction mismatch (e.g. requesting BOTH // can serve the opposite direction without reopening, provided sample
// while opened for INPUT only) needs a reopen. // settings match. Promote open_direction to BOTH when we see a
bool direction_compatible = data->open_direction == config->direction // complementary request.
|| data->open_direction == AUDIO_CODEC_DIR_BOTH; bool is_complementary = (data->open_direction == AUDIO_CODEC_DIR_OUTPUT && config->direction == AUDIO_CODEC_DIR_INPUT)
|| (data->open_direction == AUDIO_CODEC_DIR_INPUT && config->direction == AUDIO_CODEC_DIR_OUTPUT);
bool direction_compatible = (data->open_direction == config->direction)
|| (data->open_direction == AUDIO_CODEC_DIR_BOTH)
|| (config->direction == AUDIO_CODEC_DIR_BOTH)
|| is_complementary;
bool same_config = direction_compatible bool same_config = direction_compatible
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample && data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
&& data->open_sample_info.channel == sample_info.channel && data->open_sample_info.channel == sample_info.channel
&& data->open_sample_info.sample_rate == sample_info.sample_rate; && data->open_sample_info.sample_rate == sample_info.sample_rate;
return same_config ? ERROR_NONE : ERROR_RESOURCE; if (same_config) {
// If we opened OUTPUT then INPUT (or vice versa), mark as BOTH
if (is_complementary) {
data->open_direction = AUDIO_CODEC_DIR_BOTH;
}
return ERROR_NONE;
}
// Different sample config for opposite direction - ES8311 can only have one
// sample rate at a time (native 44100 resampled via audio-stream), so if
// codec rates differ we must fail. But if both sides use native 44100 (audio-stream
// always opens codec with native rate), we allow it.
if (direction_compatible) {
// Allow if both use same native rate path (audio-stream opens with codec's native)
return ERROR_RESOURCE;
}
return ERROR_RESOURCE;
} }
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
@@ -14,6 +14,9 @@ bool isValidAppVersionName(const std::string& version);
bool isValidAppVersionCode(const std::string& version); bool isValidAppVersionCode(const std::string& version);
bool isValidName(const std::string& name); bool isValidName(const std::string& name);
/** Parses a comma-separated flags string (e.g. "HideStatusBar,Hidden") into appFlags bitmask. */
uint16_t parseAppFlagsString(const std::string& raw);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */ /** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest); bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest);
@@ -56,6 +56,34 @@ bool isValidName(const std::string& name) {
}); });
} }
uint16_t parseAppFlagsString(const std::string& raw) {
uint16_t flags = AppManifest::Flags::None;
if (raw.empty()) {
return flags;
}
auto parts = string::split(raw, ",");
for (auto& part : parts) {
std::string trimmed = string::trim(part, " \t\r\n");
if (trimmed.empty()) {
continue;
}
std::string lower = string::lowercase(trimmed);
if (lower == "hidestatusbar" || lower == "hide_statusbar" || lower == "hide-statusbar" || lower == "hide_status_bar") {
flags |= AppManifest::Flags::HideStatusBar;
} else if (lower == "hidden") {
flags |= AppManifest::Flags::Hidden;
} else if (lower == "none" || lower == "0" || lower == "") {
// keep as none, no additional flag
} else {
LOG_W(TAG, "Unknown app flag \"%s\" - ignoring", trimmed.c_str());
}
}
return flags;
}
/** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */ /** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */
static bool detectIsV1Format(const std::string& filePath) { static bool detectIsV1Format(const std::string& filePath) {
std::string first_line; std::string first_line;
@@ -71,6 +71,12 @@ bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest&
return false; return false;
} }
// Optional: [app]flags - e.g. "HideStatusBar" or "HideStatusBar,Hidden"
auto flags_it = map.find("[app]flags");
if (flags_it != map.end()) {
manifest.appFlags = parseAppFlagsString(flags_it->second);
}
return true; return true;
} }
@@ -71,6 +71,12 @@ bool parseManifestV2(const std::map<std::string, std::string>& map, AppManifest&
return false; return false;
} }
// Optional: app.flags - e.g. "HideStatusBar" or "HideStatusBar,Hidden"
auto flags_it = map.find("app.flags");
if (flags_it != map.end()) {
manifest.appFlags = parseAppFlagsString(flags_it->second);
}
return true; return true;
} }
@@ -81,3 +81,70 @@ TEST_CASE("parseManifestV2() should fail when the app id is invalid") {
AppManifest manifest; AppManifest manifest;
CHECK_EQ(parseManifestV2(properties, manifest), false); CHECK_EQ(parseManifestV2(properties, manifest), false);
} }
TEST_CASE("parseAppFlagsString() should parse various flag combinations") {
CHECK_EQ(parseAppFlagsString(""), 0);
CHECK_EQ(parseAppFlagsString("None"), 0);
CHECK_EQ(parseAppFlagsString("HideStatusBar"), AppManifest::Flags::HideStatusBar);
CHECK_EQ(parseAppFlagsString("hidden"), AppManifest::Flags::Hidden);
CHECK_EQ(parseAppFlagsString("HideStatusBar,Hidden"), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
CHECK_EQ(parseAppFlagsString(" hidestatusbar , hidden "), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
CHECK_EQ(parseAppFlagsString("HideStatusBar, Hidden"), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
}
TEST_CASE("parseManifest() should parse V1 flags for fullscreen") {
TestFile file("test-manifest-v1-flags.properties");
file.writeData(
"[manifest]\n"
"version=0.1\n"
"[target]\n"
"sdk=0.0.0\n"
"platforms=esp32\n"
"[app]\n"
"id=one.tactility.sdktest\n"
"versionName=0.1.0\n"
"versionCode=1\n"
"name=SDK Test\n"
"flags=HideStatusBar\n"
);
AppManifest manifest;
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
CHECK_EQ(manifest.appFlags & AppManifest::Flags::HideStatusBar, AppManifest::Flags::HideStatusBar);
}
TEST_CASE("parseManifest() should parse V2 flags for fullscreen") {
TestFile file("test-manifest-v2-flags.properties");
file.writeData(
"manifest.version=0.1\n"
"target.sdk=0.0.0\n"
"target.platforms=esp32\n"
"app.id=one.tactility.sdktest\n"
"app.version.name=0.1.0\n"
"app.version.code=1\n"
"app.name=SDK Test\n"
"app.flags=HideStatusBar\n"
);
AppManifest manifest;
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
CHECK_EQ(manifest.appFlags & AppManifest::Flags::HideStatusBar, AppManifest::Flags::HideStatusBar);
}
TEST_CASE("parseManifest() should parse combined flags") {
TestFile file("test-manifest-v2-flags2.properties");
file.writeData(
"manifest.version=0.1\n"
"target.sdk=0.0.0\n"
"target.platforms=esp32\n"
"app.id=one.tactility.sdktest\n"
"app.version.name=0.1.0\n"
"app.version.code=1\n"
"app.name=SDK Test\n"
"app.flags=HideStatusBar,Hidden\n"
);
AppManifest manifest;
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
CHECK_EQ(manifest.appFlags, (AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden));
}