Initial commit
This commit is contained in:
386
components/ui/ui.c
Normal file
386
components/ui/ui.c
Normal file
@@ -0,0 +1,386 @@
|
||||
#include "ui.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define TAG "ui"
|
||||
|
||||
#define OLED_CONTROL_CMD 0x00
|
||||
#define OLED_CONTROL_DATA 0x40
|
||||
#define I2C_TIMEOUT_MS 50
|
||||
|
||||
static i2c_port_t s_port = CONFIG_DISPLAY_I2C_PORT;
|
||||
static uint8_t s_addr = CONFIG_DISPLAY_I2C_ADDR;
|
||||
static int s_reset_pin = CONFIG_DISPLAY_PIN_RST;
|
||||
static uint8_t s_framebuf[CONFIG_DISPLAY_WIDTH * CONFIG_DISPLAY_HEIGHT / 8];
|
||||
static char s_role[32] = "LoRa";
|
||||
static bool s_i2c_ok = false;
|
||||
static bool s_dirty = true;
|
||||
static char s_prev_lines[6][48] = {{0}};
|
||||
static char s_prev_role[sizeof(s_role)] = {0};
|
||||
|
||||
// 5x7 font (ASCII 32..127), taken from a minimal public-domain font
|
||||
static const uint8_t font5x7[] = {
|
||||
0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x5F,0x00,0x00, 0x00,0x07,0x00,0x07,0x00, 0x14,0x7F,0x14,0x7F,0x14,
|
||||
0x24,0x2A,0x7F,0x2A,0x12, 0x23,0x13,0x08,0x64,0x62, 0x36,0x49,0x55,0x22,0x50, 0x00,0x05,0x03,0x00,0x00,
|
||||
0x00,0x1C,0x22,0x41,0x00, 0x00,0x41,0x22,0x1C,0x00, 0x14,0x08,0x3E,0x08,0x14, 0x08,0x08,0x3E,0x08,0x08,
|
||||
0x00,0x50,0x30,0x00,0x00, 0x08,0x08,0x08,0x08,0x08, 0x00,0x60,0x60,0x00,0x00, 0x20,0x10,0x08,0x04,0x02,
|
||||
0x3E,0x51,0x49,0x45,0x3E, 0x00,0x42,0x7F,0x40,0x00, 0x72,0x49,0x49,0x49,0x46, 0x21,0x41,0x45,0x4B,0x31,
|
||||
0x18,0x14,0x12,0x7F,0x10, 0x27,0x45,0x45,0x45,0x39, 0x3C,0x4A,0x49,0x49,0x30, 0x01,0x71,0x09,0x05,0x03,
|
||||
0x36,0x49,0x49,0x49,0x36, 0x06,0x49,0x49,0x29,0x1E, 0x00,0x36,0x36,0x00,0x00, 0x00,0x56,0x36,0x00,0x00,
|
||||
0x08,0x14,0x22,0x41,0x00, 0x14,0x14,0x14,0x14,0x14, 0x00,0x41,0x22,0x14,0x08, 0x02,0x01,0x59,0x09,0x06,
|
||||
0x3E,0x41,0x5D,0x59,0x4E, 0x7E,0x11,0x11,0x11,0x7E, 0x7F,0x49,0x49,0x49,0x36, 0x3E,0x41,0x41,0x41,0x22,
|
||||
0x7F,0x41,0x41,0x22,0x1C, 0x7F,0x49,0x49,0x49,0x41, 0x7F,0x09,0x09,0x09,0x01, 0x3E,0x41,0x49,0x49,0x7A,
|
||||
0x7F,0x08,0x08,0x08,0x7F, 0x00,0x41,0x7F,0x41,0x00, 0x20,0x40,0x41,0x3F,0x01, 0x7F,0x08,0x14,0x22,0x41,
|
||||
0x7F,0x40,0x40,0x40,0x40, 0x7F,0x02,0x04,0x02,0x7F, 0x7F,0x04,0x08,0x10,0x7F, 0x3E,0x41,0x41,0x41,0x3E,
|
||||
0x7F,0x09,0x09,0x09,0x06, 0x3E,0x41,0x51,0x21,0x5E, 0x7F,0x09,0x19,0x29,0x46, 0x46,0x49,0x49,0x49,0x31,
|
||||
0x01,0x01,0x7F,0x01,0x01, 0x3F,0x40,0x40,0x40,0x3F, 0x1F,0x20,0x40,0x20,0x1F, 0x7F,0x20,0x18,0x20,0x7F,
|
||||
0x63,0x14,0x08,0x14,0x63, 0x03,0x04,0x78,0x04,0x03, 0x61,0x51,0x49,0x45,0x43, 0x00,0x7F,0x41,0x41,0x00,
|
||||
0x02,0x04,0x08,0x10,0x20, 0x00,0x41,0x41,0x7F,0x00, 0x04,0x02,0x01,0x02,0x04, 0x40,0x40,0x40,0x40,0x40,
|
||||
0x00,0x01,0x02,0x04,0x00, 0x20,0x54,0x54,0x54,0x78, 0x7F,0x48,0x44,0x44,0x38, 0x38,0x44,0x44,0x44,0x28,
|
||||
0x38,0x44,0x44,0x48,0x7F, 0x38,0x54,0x54,0x54,0x18, 0x08,0x7E,0x09,0x01,0x02, 0x0C,0x52,0x52,0x52,0x3E,
|
||||
0x7F,0x08,0x04,0x04,0x78, 0x00,0x44,0x7D,0x40,0x00, 0x20,0x40,0x40,0x3D,0x00, 0x7F,0x10,0x28,0x44,0x00,
|
||||
0x00,0x41,0x7F,0x40,0x00, 0x7C,0x04,0x18,0x04,0x78, 0x7C,0x08,0x04,0x04,0x78, 0x38,0x44,0x44,0x44,0x38,
|
||||
0x7C,0x14,0x14,0x14,0x08, 0x08,0x14,0x14,0x18,0x7C, 0x7C,0x08,0x04,0x04,0x08, 0x48,0x54,0x54,0x54,0x24,
|
||||
0x04,0x3F,0x44,0x40,0x20, 0x3C,0x40,0x40,0x20,0x7C, 0x1C,0x20,0x40,0x20,0x1C, 0x3C,0x40,0x30,0x40,0x3C,
|
||||
0x44,0x28,0x10,0x28,0x44, 0x0C,0x50,0x50,0x50,0x3C, 0x44,0x64,0x54,0x4C,0x44, 0x00,0x08,0x36,0x41,0x00,
|
||||
0x00,0x00,0x7F,0x00,0x00, 0x00,0x41,0x36,0x08,0x00, 0x10,0x08,0x08,0x10,0x08, 0x78,0x46,0x41,0x46,0x78
|
||||
};
|
||||
|
||||
static esp_err_t ssd1306_write(uint8_t control, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_i2c_ok) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (!data || len == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
static uint8_t buf[CONFIG_DISPLAY_WIDTH + 2];
|
||||
if (len + 1 > sizeof(buf)) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
buf[0] = control;
|
||||
memcpy(&buf[1], data, len);
|
||||
return i2c_master_write_to_device(s_port, s_addr, buf, len + 1, pdMS_TO_TICKS(I2C_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
static inline esp_err_t ssd1306_send_cmd(uint8_t cmd)
|
||||
{
|
||||
if (!s_i2c_ok) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ssd1306_write(OLED_CONTROL_CMD, &cmd, 1);
|
||||
}
|
||||
|
||||
static inline esp_err_t ssd1306_send_cmd2(uint8_t cmd, uint8_t val)
|
||||
{
|
||||
if (!s_i2c_ok) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
uint8_t data[2] = {cmd, val};
|
||||
return ssd1306_write(OLED_CONTROL_CMD, data, sizeof(data));
|
||||
}
|
||||
|
||||
static inline esp_err_t ssd1306_send_data(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_i2c_ok) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ssd1306_write(OLED_CONTROL_DATA, data, len);
|
||||
}
|
||||
|
||||
static void i2c_init(void)
|
||||
{
|
||||
s_i2c_ok = false;
|
||||
if (s_reset_pin == CONFIG_DISPLAY_PIN_SCL || s_reset_pin == CONFIG_DISPLAY_PIN_SDA) {
|
||||
ESP_LOGW(TAG, "RST pin matches I2C pin (RST=%d, SDA=%d, SCL=%d) — ensure wiring is correct", s_reset_pin, CONFIG_DISPLAY_PIN_SDA, CONFIG_DISPLAY_PIN_SCL);
|
||||
}
|
||||
|
||||
// Базова валідація пінів, щоб не падати через ESP_ERR_INVALID_ARG
|
||||
if (CONFIG_DISPLAY_PIN_SDA < 0 || CONFIG_DISPLAY_PIN_SCL < 0 ||
|
||||
CONFIG_DISPLAY_PIN_SDA > 48 || CONFIG_DISPLAY_PIN_SCL > 48 ||
|
||||
CONFIG_DISPLAY_PIN_SDA == CONFIG_DISPLAY_PIN_SCL ||
|
||||
CONFIG_DISPLAY_PIN_SDA == 45 || CONFIG_DISPLAY_PIN_SDA == 46 ||
|
||||
CONFIG_DISPLAY_PIN_SCL == 45 || CONFIG_DISPLAY_PIN_SCL == 46) {
|
||||
ESP_LOGE(TAG, "Invalid I2C pins: SDA=%d SCL=%d (port %d)",
|
||||
CONFIG_DISPLAY_PIN_SDA, CONFIG_DISPLAY_PIN_SCL, s_port);
|
||||
return;
|
||||
}
|
||||
|
||||
i2c_config_t conf = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = CONFIG_DISPLAY_PIN_SDA,
|
||||
.scl_io_num = CONFIG_DISPLAY_PIN_SCL,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
};
|
||||
conf.master.clk_speed = 400000;
|
||||
esp_err_t err = i2c_param_config(s_port, &conf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2c_param_config failed: SDA=%d SCL=%d port=%d err=%s",
|
||||
CONFIG_DISPLAY_PIN_SDA, CONFIG_DISPLAY_PIN_SCL, s_port, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
err = i2c_driver_install(s_port, conf.mode, 0, 0, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2c_driver_install failed: port=%d err=%s", s_port, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
s_i2c_ok = true;
|
||||
}
|
||||
|
||||
static void oled_reset(void)
|
||||
{
|
||||
if (s_reset_pin < 0) {
|
||||
return;
|
||||
}
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << s_reset_pin,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
gpio_set_level(s_reset_pin, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
gpio_set_level(s_reset_pin, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
gpio_set_level(s_reset_pin, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
static void ssd1306_init_sequence(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Init OLED %s %dx%d at I2C%d addr 0x%02X (SDA=%d, SCL=%d, RST=%d)",
|
||||
CONFIG_DISPLAY_DRIVER,
|
||||
CONFIG_DISPLAY_WIDTH,
|
||||
CONFIG_DISPLAY_HEIGHT,
|
||||
s_port,
|
||||
s_addr,
|
||||
CONFIG_DISPLAY_PIN_SDA,
|
||||
CONFIG_DISPLAY_PIN_SCL,
|
||||
CONFIG_DISPLAY_PIN_RST);
|
||||
|
||||
uint8_t com_pins = (CONFIG_DISPLAY_HEIGHT == 32) ? 0x02 : 0x12;
|
||||
uint8_t multiplex = CONFIG_DISPLAY_HEIGHT - 1;
|
||||
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xAE)); // display off
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0xD5, 0x80)); // clock div
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0xA8, multiplex)); // multiplex
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0xD3, 0x00)); // display offset
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x40)); // start line = 0
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0x8D, 0x14)); // charge pump on
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0x20, 0x00)); // horizontal addressing
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xA1)); // segment remap
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xC8)); // COM scan dec
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0xDA, com_pins)); // COM pins config
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0x81, 0x7F)); // contrast
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0xD9, 0xF1)); // pre-charge
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd2(0xDB, 0x40)); // VCOM detect
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xA4)); // resume RAM
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xA6)); // normal display
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x2E)); // deactivate scroll
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xAF)); // display on
|
||||
|
||||
// set column/page range to avoid leftover data on wider panels
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x21));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x00));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(CONFIG_DISPLAY_WIDTH - 1));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x22));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x00));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd((CONFIG_DISPLAY_HEIGHT / 8) - 1));
|
||||
}
|
||||
|
||||
static void clear_buffer(void)
|
||||
{
|
||||
memset(s_framebuf, 0x00, sizeof(s_framebuf));
|
||||
}
|
||||
|
||||
static void draw_pixel(int x, int y, bool on)
|
||||
{
|
||||
if (x < 0 || x >= CONFIG_DISPLAY_WIDTH || y < 0 || y >= CONFIG_DISPLAY_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
int page = y / 8;
|
||||
int bit = y % 8;
|
||||
size_t idx = page * CONFIG_DISPLAY_WIDTH + x;
|
||||
if (on) {
|
||||
s_framebuf[idx] |= (1U << bit);
|
||||
} else {
|
||||
s_framebuf[idx] &= ~(1U << bit);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_char_scaled(int x, int y, char c, int scale)
|
||||
{
|
||||
if (c < 32 || c > 126) {
|
||||
c = '?';
|
||||
}
|
||||
if (scale < 1) {
|
||||
scale = 1;
|
||||
}
|
||||
const uint8_t *glyph = &font5x7[(c - 32) * 5];
|
||||
for (int col = 0; col < 5; col++) {
|
||||
uint8_t line = glyph[col];
|
||||
for (int row = 0; row < 7; row++) {
|
||||
bool pixel_on = line & (1 << row);
|
||||
for (int dx = 0; dx < scale; dx++) {
|
||||
for (int dy = 0; dy < scale; dy++) {
|
||||
int px = x + col * scale + dx;
|
||||
int py = y + row * scale + dy;
|
||||
draw_pixel(px, py, pixel_on);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// spacing column
|
||||
for (int dx = 0; dx < scale; dx++) {
|
||||
int px = x + 5 * scale + dx;
|
||||
for (int row = 0; row < 7 * scale; row++) {
|
||||
draw_pixel(px, y + row, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_text_scaled(int x, int y, const char *text, int scale)
|
||||
{
|
||||
int cursor_x = x;
|
||||
while (text && *text) {
|
||||
draw_char_scaled(cursor_x, y, *text, scale);
|
||||
cursor_x += 6 * scale;
|
||||
text++;
|
||||
}
|
||||
}
|
||||
|
||||
static void sanitize_ascii(const char *in, char *out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0) {
|
||||
return;
|
||||
}
|
||||
if (!in) {
|
||||
out[0] = '\0';
|
||||
return;
|
||||
}
|
||||
size_t out_idx = 0;
|
||||
for (size_t i = 0; in[i] != '\0' && out_idx + 1 < out_len; i++) {
|
||||
unsigned char c = (unsigned char)in[i];
|
||||
if (c >= 32 && c <= 126) {
|
||||
out[out_idx++] = (char)c;
|
||||
} else {
|
||||
out[out_idx++] = ' ';
|
||||
}
|
||||
}
|
||||
out[out_idx] = '\0';
|
||||
}
|
||||
|
||||
static void sanitize_role(const char *role)
|
||||
{
|
||||
if (!role || role[0] == '\0') {
|
||||
strlcpy(s_role, "LoRa", sizeof(s_role));
|
||||
return;
|
||||
}
|
||||
if (strstr(role, "ередавач")) {
|
||||
strlcpy(s_role, "TX", sizeof(s_role));
|
||||
return;
|
||||
}
|
||||
if (strstr(role, "риймач")) {
|
||||
strlcpy(s_role, "RX", sizeof(s_role));
|
||||
return;
|
||||
}
|
||||
sanitize_ascii(role, s_role, sizeof(s_role));
|
||||
if (s_role[0] == '\0') {
|
||||
strlcpy(s_role, "LoRa", sizeof(s_role));
|
||||
}
|
||||
}
|
||||
|
||||
static void ssd1306_flush(void)
|
||||
{
|
||||
const int pages = CONFIG_DISPLAY_HEIGHT / 8;
|
||||
for (int page = 0; page < pages; page++) {
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0xB0 | page));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x00));
|
||||
ESP_ERROR_CHECK(ssd1306_send_cmd(0x10));
|
||||
const uint8_t *line = &s_framebuf[page * CONFIG_DISPLAY_WIDTH];
|
||||
ESP_ERROR_CHECK(ssd1306_send_data(line, CONFIG_DISPLAY_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
void ui_init(void)
|
||||
{
|
||||
i2c_init();
|
||||
if (!s_i2c_ok) {
|
||||
ESP_LOGE(TAG, "UI disabled: I2C init failed");
|
||||
return;
|
||||
}
|
||||
oled_reset();
|
||||
ssd1306_init_sequence();
|
||||
clear_buffer();
|
||||
ssd1306_flush();
|
||||
s_dirty = true;
|
||||
}
|
||||
|
||||
void ui_show_role(const char *role)
|
||||
{
|
||||
if (!s_i2c_ok) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "UI: role %s", role);
|
||||
sanitize_role(role);
|
||||
s_dirty = true;
|
||||
}
|
||||
|
||||
void ui_show_status(const char *line1, const char *line2, const char *line3, const char *line4, const char *line5, const char *line6)
|
||||
{
|
||||
if (!s_i2c_ok) {
|
||||
return;
|
||||
}
|
||||
const char *lines[6] = {line1, line2, line3, line4, line5, line6};
|
||||
char safe[6][48] = {{0}};
|
||||
bool changed = s_dirty;
|
||||
|
||||
if (strncmp(s_prev_role, s_role, sizeof(s_prev_role)) != 0) {
|
||||
changed = true;
|
||||
strlcpy(s_prev_role, s_role, sizeof(s_prev_role));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sanitize_ascii(lines[i], safe[i], sizeof(safe[i]));
|
||||
if (strncmp(s_prev_lines[i], safe[i], sizeof(s_prev_lines[i])) != 0) {
|
||||
changed = true;
|
||||
strlcpy(s_prev_lines[i], safe[i], sizeof(s_prev_lines[i]));
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear_buffer();
|
||||
|
||||
// Header
|
||||
draw_text_scaled(0, 0, s_role, 2);
|
||||
|
||||
// Рядки починаються нижче заголовка, щоб не накладались
|
||||
int y = 16;
|
||||
const int row_height = 8;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (safe[i][0] != '\0') {
|
||||
draw_text_scaled(0, y, safe[i], 1);
|
||||
}
|
||||
y += row_height + 2;
|
||||
}
|
||||
|
||||
ssd1306_flush();
|
||||
s_dirty = false;
|
||||
ESP_LOGI(TAG, "UI: %s | %s | %s | %s | %s | %s",
|
||||
safe[0], safe[1], safe[2], safe[3], safe[4], safe[5]);
|
||||
}
|
||||
Reference in New Issue
Block a user