Files
ESP32_Serial_Swiss_Army_Knife/src/local_display.c
T

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;
}
}
}