657 lines
19 KiB
C
657 lines
19 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* Bounded SSD1315-compatible OLED service with separate physical panels. */
|
|
|
|
#include "local_display.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include "board_pins.h"
|
|
#include "driver/i2c_master.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "freertos/task.h"
|
|
|
|
#define LOCAL_DISPLAY_PAGE_COUNT (LOCAL_DISPLAY_HEIGHT / 8U)
|
|
#define LOCAL_DISPLAY_FRAMEBUFFER_SIZE (LOCAL_DISPLAY_WIDTH * LOCAL_DISPLAY_PAGE_COUNT)
|
|
#define LOCAL_DISPLAY_I2C_SPEED_HZ 100000U
|
|
#define LOCAL_DISPLAY_I2C_TIMEOUT_MS 250U
|
|
#define LOCAL_DISPLAY_PROBE_TIMEOUT_MS 50U
|
|
#define LOCAL_DISPLAY_SCAN_TIMEOUT_MS 10U
|
|
#define LOCAL_DISPLAY_LOCK_TIMEOUT_MS 3000U
|
|
#define LOCAL_DISPLAY_DEFAULT_CONTRAST 127U
|
|
#define LOCAL_DISPLAY_COMMAND_CAPACITY 32U
|
|
#define LOCAL_DISPLAY_DATA_CHUNK_SIZE 128U
|
|
#define LOCAL_DISPLAY_FLUSH_BUDGET_US (500LL * 1000LL)
|
|
|
|
static const uint8_t s_expected_addresses[] = {0x3cU, 0x3dU};
|
|
|
|
typedef struct {
|
|
char character;
|
|
uint8_t columns[5];
|
|
} glyph_t;
|
|
|
|
/* Compact project-owned 5x7 ASCII subset with distinct upper/lower-case glyphs. */
|
|
static const glyph_t s_glyphs[] = {
|
|
{' ', {0x00, 0x00, 0x00, 0x00, 0x00}},
|
|
{'!', {0x00, 0x00, 0x5f, 0x00, 0x00}},
|
|
{'-', {0x08, 0x08, 0x08, 0x08, 0x08}},
|
|
{'.', {0x00, 0x60, 0x60, 0x00, 0x00}},
|
|
{'/', {0x20, 0x10, 0x08, 0x04, 0x02}},
|
|
{':', {0x00, 0x36, 0x36, 0x00, 0x00}},
|
|
{'>', {0x00, 0x41, 0x22, 0x14, 0x08}},
|
|
{'?', {0x02, 0x01, 0x51, 0x09, 0x06}},
|
|
{'_', {0x40, 0x40, 0x40, 0x40, 0x40}},
|
|
{'0', {0x3e, 0x51, 0x49, 0x45, 0x3e}},
|
|
{'1', {0x00, 0x42, 0x7f, 0x40, 0x00}},
|
|
{'2', {0x42, 0x61, 0x51, 0x49, 0x46}},
|
|
{'3', {0x21, 0x41, 0x45, 0x4b, 0x31}},
|
|
{'4', {0x18, 0x14, 0x12, 0x7f, 0x10}},
|
|
{'5', {0x27, 0x45, 0x45, 0x45, 0x39}},
|
|
{'6', {0x3c, 0x4a, 0x49, 0x49, 0x30}},
|
|
{'7', {0x01, 0x71, 0x09, 0x05, 0x03}},
|
|
{'8', {0x36, 0x49, 0x49, 0x49, 0x36}},
|
|
{'9', {0x06, 0x49, 0x49, 0x29, 0x1e}},
|
|
{'A', {0x7e, 0x11, 0x11, 0x11, 0x7e}},
|
|
{'B', {0x7f, 0x49, 0x49, 0x49, 0x36}},
|
|
{'C', {0x3e, 0x41, 0x41, 0x41, 0x22}},
|
|
{'D', {0x7f, 0x41, 0x41, 0x22, 0x1c}},
|
|
{'E', {0x7f, 0x49, 0x49, 0x49, 0x41}},
|
|
{'F', {0x7f, 0x09, 0x09, 0x09, 0x01}},
|
|
{'G', {0x3e, 0x41, 0x49, 0x49, 0x7a}},
|
|
{'H', {0x7f, 0x08, 0x08, 0x08, 0x7f}},
|
|
{'I', {0x00, 0x41, 0x7f, 0x41, 0x00}},
|
|
{'J', {0x20, 0x40, 0x41, 0x3f, 0x01}},
|
|
{'K', {0x7f, 0x08, 0x14, 0x22, 0x41}},
|
|
{'L', {0x7f, 0x40, 0x40, 0x40, 0x40}},
|
|
{'M', {0x7f, 0x02, 0x0c, 0x02, 0x7f}},
|
|
{'N', {0x7f, 0x04, 0x08, 0x10, 0x7f}},
|
|
{'O', {0x3e, 0x41, 0x41, 0x41, 0x3e}},
|
|
{'P', {0x7f, 0x09, 0x09, 0x09, 0x06}},
|
|
{'Q', {0x3e, 0x41, 0x51, 0x21, 0x5e}},
|
|
{'R', {0x7f, 0x09, 0x19, 0x29, 0x46}},
|
|
{'S', {0x46, 0x49, 0x49, 0x49, 0x31}},
|
|
{'T', {0x01, 0x01, 0x7f, 0x01, 0x01}},
|
|
{'U', {0x3f, 0x40, 0x40, 0x40, 0x3f}},
|
|
{'V', {0x1f, 0x20, 0x40, 0x20, 0x1f}},
|
|
{'W', {0x7f, 0x20, 0x18, 0x20, 0x7f}},
|
|
{'X', {0x63, 0x14, 0x08, 0x14, 0x63}},
|
|
{'Y', {0x03, 0x04, 0x78, 0x04, 0x03}},
|
|
{'Z', {0x61, 0x51, 0x49, 0x45, 0x43}},
|
|
{'a', {0x20, 0x54, 0x54, 0x54, 0x78}},
|
|
{'b', {0x7f, 0x48, 0x44, 0x44, 0x38}},
|
|
{'c', {0x38, 0x44, 0x44, 0x44, 0x20}},
|
|
{'d', {0x38, 0x44, 0x44, 0x48, 0x7f}},
|
|
{'e', {0x38, 0x54, 0x54, 0x54, 0x18}},
|
|
{'f', {0x08, 0x7e, 0x09, 0x01, 0x02}},
|
|
{'g', {0x0c, 0x52, 0x52, 0x52, 0x3e}},
|
|
{'h', {0x7f, 0x08, 0x04, 0x04, 0x78}},
|
|
{'i', {0x00, 0x44, 0x7d, 0x40, 0x00}},
|
|
{'j', {0x20, 0x40, 0x44, 0x3d, 0x00}},
|
|
{'k', {0x7f, 0x10, 0x28, 0x44, 0x00}},
|
|
{'l', {0x00, 0x41, 0x7f, 0x40, 0x00}},
|
|
{'m', {0x7c, 0x04, 0x18, 0x04, 0x78}},
|
|
{'n', {0x7c, 0x08, 0x04, 0x04, 0x78}},
|
|
{'o', {0x38, 0x44, 0x44, 0x44, 0x38}},
|
|
{'p', {0x7c, 0x14, 0x14, 0x14, 0x08}},
|
|
{'q', {0x08, 0x14, 0x14, 0x18, 0x7c}},
|
|
{'r', {0x7c, 0x08, 0x04, 0x04, 0x08}},
|
|
{'s', {0x48, 0x54, 0x54, 0x54, 0x20}},
|
|
{'t', {0x04, 0x3f, 0x44, 0x40, 0x20}},
|
|
{'u', {0x3c, 0x40, 0x40, 0x20, 0x7c}},
|
|
{'v', {0x1c, 0x20, 0x40, 0x20, 0x1c}},
|
|
{'w', {0x3c, 0x40, 0x30, 0x40, 0x3c}},
|
|
{'x', {0x44, 0x28, 0x10, 0x28, 0x44}},
|
|
{'y', {0x0c, 0x50, 0x50, 0x50, 0x3c}},
|
|
{'z', {0x44, 0x64, 0x54, 0x4c, 0x44}},
|
|
};
|
|
|
|
static i2c_master_bus_handle_t s_bus;
|
|
static i2c_master_dev_handle_t s_device;
|
|
static StaticSemaphore_t s_mutex_storage;
|
|
static SemaphoreHandle_t s_mutex;
|
|
static uint8_t s_framebuffer[LOCAL_DISPLAY_FRAMEBUFFER_SIZE];
|
|
static bool s_bus_ready;
|
|
static bool s_initialized;
|
|
static bool s_frame_active;
|
|
static TaskHandle_t s_frame_owner;
|
|
static uint8_t s_address;
|
|
static uint8_t s_contrast = LOCAL_DISPLAY_DEFAULT_CONTRAST;
|
|
static bool s_inverted;
|
|
static uint8_t s_dirty_pages;
|
|
static esp_err_t s_last_error = ESP_ERR_INVALID_STATE;
|
|
|
|
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
|
|
{
|
|
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
|
|
return (milliseconds > 0U && ticks == 0U) ? 1U : ticks;
|
|
}
|
|
|
|
static esp_err_t take_lock(void)
|
|
{
|
|
if (s_mutex == NULL) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
return xSemaphoreTake(s_mutex, milliseconds_to_ticks(LOCAL_DISPLAY_LOCK_TIMEOUT_MS)) == pdTRUE
|
|
? ESP_OK
|
|
: ESP_ERR_TIMEOUT;
|
|
}
|
|
|
|
static void give_lock(void)
|
|
{
|
|
if (s_mutex != NULL) {
|
|
(void)xSemaphoreGive(s_mutex);
|
|
}
|
|
}
|
|
|
|
static void set_last_error(esp_err_t error)
|
|
{
|
|
s_last_error = error;
|
|
}
|
|
|
|
static esp_err_t send_commands_locked(const uint8_t *commands, size_t count)
|
|
{
|
|
if (!s_initialized || s_device == NULL || commands == NULL || count == 0U ||
|
|
count > LOCAL_DISPLAY_COMMAND_CAPACITY) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
uint8_t transfer[LOCAL_DISPLAY_COMMAND_CAPACITY + 1U];
|
|
transfer[0] = 0x00U;
|
|
memcpy(&transfer[1], commands, count);
|
|
return i2c_master_transmit(s_device, transfer, count + 1U,
|
|
LOCAL_DISPLAY_I2C_TIMEOUT_MS);
|
|
}
|
|
|
|
static esp_err_t send_command_locked(uint8_t command)
|
|
{
|
|
return send_commands_locked(&command, 1U);
|
|
}
|
|
|
|
static esp_err_t flush_dirty_locked(void)
|
|
{
|
|
if (!s_initialized || s_device == NULL) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
int64_t started = esp_timer_get_time();
|
|
uint8_t transfer[LOCAL_DISPLAY_DATA_CHUNK_SIZE + 1U];
|
|
transfer[0] = 0x40U;
|
|
for (uint8_t page = 0U; page < LOCAL_DISPLAY_PAGE_COUNT; ++page) {
|
|
uint8_t page_mask = (uint8_t)(1U << page);
|
|
if ((s_dirty_pages & page_mask) == 0U) {
|
|
continue;
|
|
}
|
|
if ((esp_timer_get_time() - started) >= LOCAL_DISPLAY_FLUSH_BUDGET_US) {
|
|
return ESP_ERR_TIMEOUT;
|
|
}
|
|
|
|
const uint8_t commands[] = {
|
|
0x21U, 0x00U, (uint8_t)(LOCAL_DISPLAY_WIDTH - 1U),
|
|
0x22U, page, page,
|
|
};
|
|
esp_err_t error = send_commands_locked(commands, sizeof(commands));
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
if ((esp_timer_get_time() - started) >= LOCAL_DISPLAY_FLUSH_BUDGET_US) {
|
|
return ESP_ERR_TIMEOUT;
|
|
}
|
|
memcpy(&transfer[1], &s_framebuffer[(size_t)page * LOCAL_DISPLAY_WIDTH],
|
|
LOCAL_DISPLAY_WIDTH);
|
|
error = i2c_master_transmit(s_device, transfer, sizeof(transfer),
|
|
LOCAL_DISPLAY_I2C_TIMEOUT_MS);
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
s_dirty_pages &= (uint8_t)~page_mask;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static void set_pixel_raw(uint8_t x, uint8_t y, bool on)
|
|
{
|
|
if (x >= LOCAL_DISPLAY_WIDTH || y >= LOCAL_DISPLAY_HEIGHT) {
|
|
return;
|
|
}
|
|
size_t index = (size_t)(y / 8U) * LOCAL_DISPLAY_WIDTH + x;
|
|
uint8_t mask = (uint8_t)(1U << (y & 7U));
|
|
uint8_t before = s_framebuffer[index];
|
|
if (on) {
|
|
s_framebuffer[index] |= mask;
|
|
} else {
|
|
s_framebuffer[index] &= (uint8_t)~mask;
|
|
}
|
|
if (before != s_framebuffer[index]) {
|
|
s_dirty_pages |= (uint8_t)(1U << (y / 8U));
|
|
}
|
|
}
|
|
|
|
static bool panel_geometry(local_display_panel_t panel, uint8_t *origin_y, uint8_t *height)
|
|
{
|
|
if (origin_y == NULL || height == NULL) {
|
|
return false;
|
|
}
|
|
switch (panel) {
|
|
case LOCAL_DISPLAY_PANEL_STATUS:
|
|
*origin_y = 0U;
|
|
*height = LOCAL_DISPLAY_STATUS_HEIGHT;
|
|
return true;
|
|
case LOCAL_DISPLAY_PANEL_CONTENT:
|
|
*origin_y = LOCAL_DISPLAY_STATUS_HEIGHT;
|
|
*height = LOCAL_DISPLAY_CONTENT_HEIGHT;
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static const glyph_t *find_glyph(char character)
|
|
{
|
|
for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) {
|
|
if (s_glyphs[index].character == character) {
|
|
return &s_glyphs[index];
|
|
}
|
|
}
|
|
for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) {
|
|
if (s_glyphs[index].character == '?') {
|
|
return &s_glyphs[index];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static esp_err_t select_device_locked(uint8_t address)
|
|
{
|
|
if (address != 0x3cU && address != 0x3dU) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
esp_err_t error = i2c_master_probe(s_bus, address, LOCAL_DISPLAY_PROBE_TIMEOUT_MS);
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
if (s_device != NULL && s_address == address) {
|
|
return ESP_OK;
|
|
}
|
|
if (s_device != NULL) {
|
|
error = i2c_master_bus_rm_device(s_device);
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
s_device = NULL;
|
|
s_initialized = false;
|
|
}
|
|
|
|
const i2c_device_config_t config = {
|
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
|
.device_address = address,
|
|
.scl_speed_hz = LOCAL_DISPLAY_I2C_SPEED_HZ,
|
|
};
|
|
error = i2c_master_bus_add_device(s_bus, &config, &s_device);
|
|
if (error == ESP_OK) {
|
|
s_address = address;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static esp_err_t initialize_locked(uint8_t address)
|
|
{
|
|
s_initialized = false;
|
|
esp_err_t error = select_device_locked(address);
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
|
|
const uint8_t commands[] = {
|
|
0xaeU, 0xd5U, 0x80U, 0xa8U, 0x3fU, 0xd3U, 0x00U, 0x40U,
|
|
0x8dU, 0x14U, 0x20U, 0x00U, 0xa1U, 0xc8U, 0xdaU, 0x12U,
|
|
0x81U, LOCAL_DISPLAY_DEFAULT_CONTRAST, 0xd9U, 0xf1U, 0xdbU,
|
|
0x40U, 0xa4U, 0xa6U, 0x2eU,
|
|
};
|
|
|
|
/* send_commands_locked requires initialized, so issue the bootstrap directly. */
|
|
uint8_t transfer[sizeof(commands) + 1U];
|
|
transfer[0] = 0x00U;
|
|
memcpy(&transfer[1], commands, sizeof(commands));
|
|
error = i2c_master_transmit(s_device, transfer, sizeof(transfer),
|
|
LOCAL_DISPLAY_I2C_TIMEOUT_MS);
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
|
|
s_initialized = true;
|
|
s_contrast = LOCAL_DISPLAY_DEFAULT_CONTRAST;
|
|
s_inverted = false;
|
|
memset(s_framebuffer, 0, sizeof(s_framebuffer));
|
|
s_dirty_pages = (uint8_t)((1U << LOCAL_DISPLAY_PAGE_COUNT) - 1U);
|
|
error = flush_dirty_locked();
|
|
if (error == ESP_OK) {
|
|
error = send_command_locked(0xafU);
|
|
}
|
|
if (error != ESP_OK) {
|
|
s_initialized = false;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_init(void)
|
|
{
|
|
if (s_bus_ready || s_mutex != NULL) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
s_mutex = xSemaphoreCreateMutexStatic(&s_mutex_storage);
|
|
if (s_mutex == NULL) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
const i2c_master_bus_config_t config = {
|
|
.i2c_port = LOCAL_UI_I2C_PORT,
|
|
.sda_io_num = LOCAL_UI_DISPLAY_SDA_GPIO,
|
|
.scl_io_num = LOCAL_UI_DISPLAY_SCL_GPIO,
|
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
|
.glitch_ignore_cnt = 7,
|
|
.flags.enable_internal_pullup = false,
|
|
};
|
|
esp_err_t error = i2c_new_master_bus(&config, &s_bus);
|
|
if (error != ESP_OK) {
|
|
vSemaphoreDelete(s_mutex);
|
|
s_mutex = NULL;
|
|
set_last_error(error);
|
|
return error;
|
|
}
|
|
s_bus_ready = true;
|
|
set_last_error(ESP_OK);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t local_display_probe_expected(uint8_t *address_7bit)
|
|
{
|
|
if (address_7bit == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (!s_bus_ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
|
|
uint8_t found = 0U;
|
|
for (size_t index = 0U; index < sizeof(s_expected_addresses) / sizeof(s_expected_addresses[0]); ++index) {
|
|
uint8_t address = s_expected_addresses[index];
|
|
if (i2c_master_probe(s_bus, address, LOCAL_DISPLAY_PROBE_TIMEOUT_MS) == ESP_OK) {
|
|
if (found == 0U) {
|
|
*address_7bit = address;
|
|
}
|
|
++found;
|
|
}
|
|
}
|
|
error = found == 0U ? ESP_ERR_NOT_FOUND : ESP_OK;
|
|
set_last_error(error);
|
|
give_lock();
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_start_at(uint8_t address_7bit)
|
|
{
|
|
if (!s_bus_ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
error = initialize_locked(address_7bit);
|
|
set_last_error(error);
|
|
give_lock();
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_start(void)
|
|
{
|
|
uint8_t address = 0U;
|
|
esp_err_t error = local_display_probe_expected(&address);
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
return local_display_start_at(address);
|
|
}
|
|
|
|
esp_err_t local_display_stop(void)
|
|
{
|
|
if (!s_bus_ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
if (!s_initialized) {
|
|
error = ESP_ERR_INVALID_STATE;
|
|
} else {
|
|
error = send_command_locked(0xaeU);
|
|
/* A failed command leaves panel state unknown; force a clean reinit. */
|
|
s_initialized = false;
|
|
}
|
|
set_last_error(error);
|
|
give_lock();
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_get_snapshot(local_display_snapshot_t *snapshot)
|
|
{
|
|
if (snapshot == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
*snapshot = (local_display_snapshot_t){
|
|
.bus_ready = s_bus_ready,
|
|
.initialized = s_initialized,
|
|
.address_7bit = s_address,
|
|
.contrast = s_contrast,
|
|
.inverted = s_inverted,
|
|
.dirty_page_mask = s_dirty_pages,
|
|
.last_error = s_last_error,
|
|
};
|
|
give_lock();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t local_display_scan(local_display_scan_callback_t callback,
|
|
void *context,
|
|
size_t *responding_count)
|
|
{
|
|
if (!s_bus_ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
|
|
uint8_t responses[0x78U - 0x08U];
|
|
size_t found = 0U;
|
|
for (uint16_t address = 0x08U; address <= 0x77U; ++address) {
|
|
if (i2c_master_probe(s_bus, address, LOCAL_DISPLAY_SCAN_TIMEOUT_MS) == ESP_OK) {
|
|
responses[found++] = (uint8_t)address;
|
|
}
|
|
vTaskDelay(1U);
|
|
}
|
|
if (responding_count != NULL) {
|
|
*responding_count = found;
|
|
}
|
|
error = found == 0U ? ESP_ERR_NOT_FOUND : ESP_OK;
|
|
set_last_error(error);
|
|
give_lock();
|
|
|
|
/* Callers may safely use the display service from the callback. */
|
|
if (callback != NULL) {
|
|
for (size_t index = 0U; index < found; ++index) {
|
|
callback(responses[index], context);
|
|
}
|
|
}
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_set_contrast(uint8_t contrast)
|
|
{
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
const uint8_t commands[] = {0x81U, contrast};
|
|
error = send_commands_locked(commands, sizeof(commands));
|
|
if (error == ESP_OK) {
|
|
s_contrast = contrast;
|
|
} else {
|
|
s_initialized = false;
|
|
}
|
|
set_last_error(error);
|
|
give_lock();
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_set_inverted(bool inverted)
|
|
{
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
error = send_command_locked(inverted ? 0xa7U : 0xa6U);
|
|
if (error == ESP_OK) {
|
|
s_inverted = inverted;
|
|
} else {
|
|
s_initialized = false;
|
|
}
|
|
set_last_error(error);
|
|
give_lock();
|
|
return error;
|
|
}
|
|
|
|
esp_err_t local_display_frame_begin(void)
|
|
{
|
|
esp_err_t error = take_lock();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
if (!s_initialized || s_frame_active) {
|
|
give_lock();
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
s_frame_active = true;
|
|
s_frame_owner = xTaskGetCurrentTaskHandle();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t local_display_frame_end(void)
|
|
{
|
|
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
esp_err_t error = flush_dirty_locked();
|
|
if (error != ESP_OK) {
|
|
s_initialized = false;
|
|
}
|
|
set_last_error(error);
|
|
s_frame_active = false;
|
|
s_frame_owner = NULL;
|
|
give_lock();
|
|
return error;
|
|
}
|
|
|
|
void local_display_frame_cancel(void)
|
|
{
|
|
if (s_frame_active && s_frame_owner == xTaskGetCurrentTaskHandle()) {
|
|
s_frame_active = false;
|
|
s_frame_owner = NULL;
|
|
give_lock();
|
|
}
|
|
}
|
|
|
|
void local_display_frame_clear(local_display_panel_t panel)
|
|
{
|
|
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
|
|
return;
|
|
}
|
|
uint8_t origin_y = 0U;
|
|
uint8_t height = 0U;
|
|
if (!panel_geometry(panel, &origin_y, &height)) {
|
|
return;
|
|
}
|
|
for (uint8_t y = origin_y; y < origin_y + height; ++y) {
|
|
for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) {
|
|
set_pixel_raw(x, y, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void local_display_frame_clear_all(void)
|
|
{
|
|
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
|
|
return;
|
|
}
|
|
for (size_t index = 0U; index < sizeof(s_framebuffer); ++index) {
|
|
if (s_framebuffer[index] != 0U) {
|
|
s_framebuffer[index] = 0U;
|
|
s_dirty_pages |= (uint8_t)(1U << (index / LOCAL_DISPLAY_WIDTH));
|
|
}
|
|
}
|
|
}
|
|
|
|
void local_display_frame_set_pixel(local_display_panel_t panel,
|
|
uint8_t x,
|
|
uint8_t y,
|
|
bool on)
|
|
{
|
|
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
|
|
return;
|
|
}
|
|
uint8_t origin_y = 0U;
|
|
uint8_t height = 0U;
|
|
if (!panel_geometry(panel, &origin_y, &height) || x >= LOCAL_DISPLAY_WIDTH || y >= height) {
|
|
return;
|
|
}
|
|
set_pixel_raw(x, (uint8_t)(origin_y + y), on);
|
|
}
|
|
|
|
void local_display_frame_draw_text(local_display_panel_t panel,
|
|
uint8_t x,
|
|
uint8_t y,
|
|
const char *text)
|
|
{
|
|
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle() || text == NULL) {
|
|
return;
|
|
}
|
|
uint8_t origin_y = 0U;
|
|
uint8_t height = 0U;
|
|
if (!panel_geometry(panel, &origin_y, &height) || y >= height) {
|
|
return;
|
|
}
|
|
|
|
uint16_t cursor_x = x;
|
|
for (const char *character = text; *character != '\0'; ++character) {
|
|
const glyph_t *glyph = find_glyph(*character);
|
|
if (glyph == NULL || cursor_x + 5U > LOCAL_DISPLAY_WIDTH) {
|
|
break;
|
|
}
|
|
for (uint8_t column = 0U; column < 5U; ++column) {
|
|
for (uint8_t row = 0U; row < 7U; ++row) {
|
|
if ((glyph->columns[column] & (uint8_t)(1U << row)) != 0U &&
|
|
y + row < height) {
|
|
set_pixel_raw((uint8_t)(cursor_x + column),
|
|
(uint8_t)(origin_y + y + row), true);
|
|
}
|
|
}
|
|
}
|
|
cursor_x += 6U;
|
|
if (cursor_x >= LOCAL_DISPLAY_WIDTH) {
|
|
break;
|
|
}
|
|
}
|
|
}
|