Browse Source

Merge e632c526b0 into 5f3b7f25d0

pull/2568/merge
Christos Themelis 19 hours ago
committed by GitHub
parent
commit
6f6f8d4f61
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 51
      boards/esp32-s3-devkitc-1-myboard.json
  2. 6
      elecrow_partitions.csv
  3. 22
      examples/companion_radio/MyMesh.cpp
  4. 3
      examples/companion_radio/MyMesh.h
  5. 25
      examples/companion_radio/task_clock.cpp
  6. 19
      examples/companion_radio/task_lvgl.cpp
  7. 2
      examples/companion_radio/ui-new/UITask.cpp
  8. 805
      examples/companion_radio/uiManager.cpp
  9. 50
      examples/companion_radio/uiTasks.cpp
  10. 10
      examples/simple_secure_chat/main.cpp
  11. 1265
      examples/simple_secure_chat_ui/main.cpp
  12. 15
      examples/simple_secure_chat_ui/messageStore.h
  13. 25
      examples/simple_secure_chat_ui/task_clock.cpp
  14. 19
      examples/simple_secure_chat_ui/task_lvgl.cpp
  15. 1656
      examples/simple_secure_chat_ui/uiManager.cpp
  16. 50
      examples/simple_secure_chat_ui/uiTasks.cpp
  17. 217
      include/lgfx.h
  18. 107
      include/lvBase.h
  19. 12
      include/lvButton.h
  20. 23
      include/lvDropDown.h
  21. 32
      include/lvKeyboard.h
  22. 41
      include/lvLabel.h
  23. 28
      include/lvList.h
  24. 42
      include/lvObj.h
  25. 51
      include/lvTabView.h
  26. 52
      include/lvTextArea.h
  27. 61
      include/uiDefines.h
  28. 11
      include/uiExternals.h
  29. 145
      include/uiManager.h
  30. 12
      include/uiTasks.h
  31. 220
      include/uiTouch.h
  32. 22
      include/uiVars.h
  33. 787
      lib/lvgl/lv_conf.h
  34. 7
      partition-table-8MB.csv
  35. 25
      src/fonts/fonts.h
  36. 1244
      src/fonts/lv_font_arial_10.c
  37. 1358
      src/fonts/lv_font_arial_12.c
  38. 1687
      src/fonts/lv_font_arial_14.c
  39. 1881
      src/fonts/lv_font_arial_16.c
  40. 2201
      src/fonts/lv_font_arial_18.c
  41. 2497
      src/fonts/lv_font_arial_20.c
  42. 2710
      src/fonts/lv_font_arial_22.c
  43. 3159
      src/fonts/lv_font_arial_24.c
  44. 3501
      src/fonts/lv_font_arial_26.c
  45. 3858
      src/fonts/lv_font_arial_28.c
  46. 4317
      src/fonts/lv_font_arial_30.c
  47. 4745
      src/fonts/lv_font_arial_32.c
  48. 5195
      src/fonts/lv_font_arial_34.c
  49. 5812
      src/fonts/lv_font_arial_36.c
  50. 6302
      src/fonts/lv_font_arial_38.c
  51. 6810
      src/fonts/lv_font_arial_40.c
  52. 7541
      src/fonts/lv_font_arial_42.c
  53. 8210
      src/fonts/lv_font_arial_44.c
  54. 9797
      src/fonts/lv_font_arial_48.c
  55. 10460
      src/fonts/lv_font_arial_50.c
  56. 13677
      src/fonts/lv_font_arial_58.c
  57. 17268
      src/fonts/lv_font_arial_66.c
  58. 21373
      src/fonts/lv_font_arial_74.c
  59. 1059
      src/fonts/lv_font_arial_8.c
  60. 5
      src/helpers/BaseChatMesh.cpp
  61. 20
      src/helpers/CommonCLI.cpp
  62. 112
      src/helpers/esp32/ESPNOWRadio.cpp
  63. 307
      src/helpers/esp32/SenseCapHAL.h
  64. 27
      src/helpers/esp32/SenseCapSX1262Wrapper.h
  65. 21
      src/helpers/radiolib/RadioLibWrappers.cpp
  66. 8
      src/helpers/radiolib/RadioLibWrappers.h
  67. 2
      variants/generic-e22/variant.h
  68. 2
      variants/meshadventurer/variant.h
  69. BIN
      variants/sensecap_indicator-espnow/1.jpg
  70. BIN
      variants/sensecap_indicator-espnow/2.jpg
  71. BIN
      variants/sensecap_indicator-espnow/3.jpg
  72. BIN
      variants/sensecap_indicator-espnow/4.jpg
  73. 45
      variants/sensecap_indicator-espnow/SCIndicatorDisplay.h
  74. 35
      variants/sensecap_indicator-espnow/platformio.ini
  75. 181
      variants/sensecap_indicator-espnow/target.cpp
  76. 60
      variants/sensecap_indicator-espnow/target.h

51
boards/esp32-s3-devkitc-1-myboard.json

@ -0,0 +1,51 @@
{
"build": {
"arduino":{
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "elecrow_partitions.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [
[
"0x303A",
"0x1001"
]
],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": [
"wifi"
],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": [
"esp-builtin"
],
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Espressif ESP32-S3-DevKitC-1-N8 -ELECROW",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"require_upload_port": true,
"speed": 921600
},
"url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html",
"vendor": "Espressif"
}

6
elecrow_partitions.csv

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x300000,
spiffs, data, spiffs, 0x310000,0xE0000,
coredump, data, coredump,0x3F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x300000
5 spiffs data spiffs 0x310000 0xE0000
6 coredump data coredump 0x3F0000 0x10000

22
examples/companion_radio/MyMesh.cpp

@ -383,7 +383,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
}
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // contact is in contacts[], schedule lazy write
}
static int sort_by_recent(const void *a, const void *b) {
@ -971,6 +971,10 @@ void MyMesh::begin(bool has_display) {
const char *MyMesh::getNodeName() {
return _prefs.node_name;
}
const char* MyMesh::getFirmwareVer() { return FIRMWARE_VERSION; }
const char* MyMesh::getBuildDate() { return FIRMWARE_BUILD_DATE; }
NodePrefs *MyMesh::getNodePrefs() {
return &_prefs;
}
@ -2007,6 +2011,22 @@ void MyMesh::enterCLIRescue() {
Serial.println("========= CLI Rescue =========");
}
void MyMesh::handleCommand(const char* command) {
while (*command == ' ') command++; // skip leading space
if (strcmp(command, "erase") == 0) {
bool success = _store->formatFileSystem();
if (success) {
Serial.println(" > erase done");
} else {
Serial.println(" Error: erase failed");
}
} else if (strcmp(command, "reboot") == 0) {
board.reboot(); // doesn't return
} else {
Serial.println(" Error: unknown command");
}
}
void MyMesh::checkCLIRescueCmd() {
int len = strlen(cli_command);
while (Serial.available() && len < sizeof(cli_command)-1) {

3
examples/companion_radio/MyMesh.h

@ -92,6 +92,8 @@ public:
void startInterface(BaseSerialInterface &serial);
const char *getNodeName();
const char *getFirmwareVer();
const char *getBuildDate();
NodePrefs *getNodePrefs();
uint32_t getBLEPin();
@ -165,6 +167,7 @@ protected:
public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
void handleCommand(const char* command);
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {

25
examples/companion_radio/task_clock.cpp

@ -0,0 +1,25 @@
#include <Arduino.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiVars.h"
#define TAG "clock_task"
void clock_task(void *pvParameters) {
vTaskSuspend(NULL);
ESP_LOGI(TAG, "Clock manager: Task running on core %d", xPortGetCoreID());
uiManager->clearDateTime();
// TODO: sync clock
while (1) {
// uiManager->updateDateTime(
// myClock->getTimeStruct()
// );
// uiManager->updateValues();
vTaskDelay(DELAY_CLOCK_TASK / portTICK_PERIOD_MS);
}
}

19
examples/companion_radio/task_lvgl.cpp

@ -0,0 +1,19 @@
#include <Arduino.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiVars.h"
#define TAG "lvgl_task"
void lvgl_task(void *pvParameters) {
vTaskSuspend(NULL);
ESP_LOGI(TAG, "UI manager: Task running on core %d", xPortGetCoreID());
while (1) {
lv_timer_handler();
vTaskDelay(DELAY_LVGL_TASK / portTICK_PERIOD_MS);
}
}

2
examples/companion_radio/ui-new/UITask.cpp

@ -698,7 +698,7 @@ void UITask::shutdown(bool restart){
_board->reboot();
} else {
_display->turnOff();
radio_driver.powerOff();
//radio_driver.powerOff();
_board->powerOff();
}
}

805
examples/companion_radio/uiManager.cpp

@ -0,0 +1,805 @@
#include <Arduino.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiVars.h"
#include "uiManager.h"
#include "../src/fonts/fonts.h"
#include <helpers/ContactInfo.h>
#include <helpers/AdvertDataHelpers.h>
#if defined(LANG_GR)
const char *UIManager::days[7] = {"Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ"};
const char *UIManager::months[12] = {"Ιαν", "Φεβ", "Μαρ", "Απρ", "Μαι", "Ιουν",
"Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"};
#elif defined(LANG_EN)
const char *UIManager::days[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
const char *UIManager::months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
#endif
#define TAG "UIManager"
extern void handleCommand(char *msg);
UIManager::UIManager() {
tmp_buf = (char*)malloc(128);
lv_disp_t * dispp = lv_disp_get_default();
lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED),
false, LV_FONT_DEFAULT);
lv_disp_set_theme(dispp, theme);
ui_Screen1_screen_init();
ui____initial_actions0 = lv_obj_create(NULL);
lv_disp_load_scr(ui_Screen1);
}
void UIManager::format_time(uint32_t ts, char *buf, size_t len)
{
time_t t = ts;
struct tm *tm_info = localtime(&t);
strftime(buf, len, "%H:%M:%S", tm_info);
}
void UIManager::format_datetime(char *buf, size_t size, const struct tm *timeinfo) {
char tmp[64];
strftime(tmp, sizeof(tmp), "%a, %d %b %Y", timeinfo);
int wday = timeinfo->tm_wday; // 0=Κυρ ... 6=Σαβ
int mon = timeinfo->tm_mon; // 0=Ιαν ... 11=Δεκ
// replace %a and %b with selected language
snprintf(buf, size, "%s, %02d %s %d", days[wday], timeinfo->tm_mday, months[mon], 1900 + timeinfo->tm_year);
}
void UIManager::updateDateTime(const struct tm timeinfo) {
// TODO: Add to settings "Date format"
char date_str[50];
format_datetime(date_str, sizeof(date_str), &timeinfo);
lv_label_set_text(ui_ValueDate, date_str);
// TODO: Add to settings "Hour format"
strftime(tmp_buf, 50, "%H:%M", &timeinfo); // 24h format
//strftime(tmp_buf, 50, "%I:%M %p", &timeinfo); // 12h format
lv_label_set_text(ui_ValueTime, tmp_buf);
// TODO: Add to settings "dim at night"
// TODO: Add to settings "dim hours"
// TODO: Add to settings "dim percentage"
if (timeinfo.tm_hour > 21 || timeinfo.tm_hour < 7) {
setNightMode(true);
} else {
setNightMode(false);
}
}
void UIManager::clearDateTime() {
#if defined(LANG_EN)
uiManager->updateInfo("Clock sync...", COLOR_WHITE);
#elif defined(LANG_GR)
uiManager->updateInfo("Συγχρονισμός ώρας...", COLOR_WHITE);
#endif
lv_label_set_text(ui_ValueDate, "--- --/--/----");
lv_label_set_text(ui_ValueTime, "--:--");
}
void UIManager::timestampToTime(time_t timestamp, char *buffer, size_t buffer_size) {
struct tm *time_info;
time_info = localtime(&timestamp);
strftime(buffer, buffer_size, "%H:%M", time_info);
}
const char* UIManager::convertDegreesToDirection(int degrees) {
// Normalize degrees to [0, 360)
degrees = degrees % 360;
if (degrees < 0) degrees += 360;
#if defined(LANG_EN)
static constexpr const char* dirs[] = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
#elif defined(LANG_GR)
static constexpr const char* dirs[] = {"Β", "ΒΑ", "Α", "ΝΑ", "Ν", "ΝΔ", "Δ", "ΒΔ"};
#else
#error "No Language defined!"
#endif
// Each direction covers 45°, starting at N = 0°
int index = static_cast<int>((degrees + 22.5) / 45.0) % 8;
return dirs[index];
}
int UIManager::windSpeedToBeaufort(float speed) {
static const float limits[] = {
0.5, 1.5, 3.3, 5.5, 7.9, 10.7,
13.8, 17.1, 20.7, 24.4, 28.4, 32.6
};
for (int i = 0; i < 12; ++i)
if (speed < limits[i])
return i;
return 12;
}
void UIManager::updateValues() {
lv_label_set_text(ui_ValueTime, "--:--");
}
void UIManager::updateInfo(const char *str, uint32_t color) {
// lv_label_set_text(ui_ValueLastUpdate, str);
// lv_obj_set_style_text_color(ui_ValueLastUpdate, lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT);
}
void UIManager::addChatBubble(const char *time_str, const char *sender, const char *msg,bool is_self)
{
// Remove oldest
if (chat_count >= MAX_CHAT_MESSAGES) {
lv_obj_del(chat_items[0]);
memmove(&chat_items[0], &chat_items[1], sizeof(lv_obj_t*) * (MAX_CHAT_MESSAGES - 1));
chat_count--;
}
// Row container (align bubble left/right)
lv_obj_t *row = lv_obj_create(ui_ChannelMessages);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, 0, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_outline_width(row, 0, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row,
is_self ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
// Bubble container (COLUMN)
lv_obj_t *bubble = lv_obj_create(row);
lv_obj_set_width(bubble, lv_pct(85));
lv_obj_set_height(bubble, LV_SIZE_CONTENT);
lv_obj_set_style_radius(bubble, 12, 0);
lv_obj_set_style_pad_all(bubble, 10, 0);
lv_obj_set_style_bg_color(bubble,
is_self ? lv_color_hex(0x1E88E5) : lv_color_hex(0x2C2C2C), 0);
// IMPORTANT: vertical layout inside bubble
lv_obj_set_flex_flow(bubble, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(bubble,
LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
// Header row (sender + time)
lv_obj_t *hdr = lv_obj_create(bubble);
lv_obj_set_width(hdr, lv_pct(100));
lv_obj_set_height(hdr, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(hdr, 0, 0);
lv_obj_set_style_border_width(hdr, 0, 0);
lv_obj_set_style_pad_all(hdr, 0, 0);
lv_obj_set_style_outline_width(hdr, 0, 0);
lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(hdr,
LV_FLEX_ALIGN_SPACE_BETWEEN,
LV_FLEX_ALIGN_START, //LV_FLEX_ALIGN_CENTER, // Ευθυγράμμιση ονόματος/ώρας στον κάθετο άξονα
LV_FLEX_ALIGN_START);
lv_obj_t *lbl_sender = lv_label_create(hdr);
lv_label_set_text(lbl_sender, sender);
lv_obj_set_style_text_color(lbl_sender,
is_self ? lv_color_hex(0xE3F2FD) : lv_color_hex(0x90CAF9), 0);
lv_obj_set_style_text_font(lbl_sender, &lv_font_arial_22, 0);
lv_obj_t *lbl_time = lv_label_create(hdr);
lv_label_set_text(lbl_time, time_str);
lv_obj_set_style_text_color(lbl_time, lv_color_hex(0xB0B0B0), 0);
lv_obj_set_style_text_font(lbl_time, &lv_font_arial_20, 0);
// Message body (below header)
lv_obj_t *lbl_msg = lv_label_create(bubble);
lv_label_set_text(lbl_msg, msg);
lv_label_set_long_mode(lbl_msg, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lbl_msg, lv_pct(100));
lv_obj_set_style_text_color(lbl_msg, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(lbl_msg, &lv_font_arial_26, 0);
// Spacing between header and text
//lv_obj_set_style_margin_top(lbl_msg, 6, 0);
lv_obj_set_style_pad_row(bubble, 6, 0);
chat_items[chat_count++] = row;
lv_obj_scroll_to_view(row, LV_ANIM_ON);
}
void UIManager::addPrivateChatBubble(const char *time_str, const char *msg, bool is_self) {
lv_obj_set_style_pad_bottom(ui_ContactMessages, 20, 0);
// 1. Row container
lv_obj_t* row = LvObj(ui_ContactMessages)
.width(lv_pct(100))
.height(LV_SIZE_CONTENT)
.bgOpa(0)
.border(0)
.padAll(4)
.scrollable(false)
.flexFlow(LV_FLEX_FLOW_ROW)
.flexAlign(
is_self ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START
);
// 2. Aligner (Column) - label and time
lv_obj_t* aligner = LvObj(row)
.width(LV_SIZE_CONTENT)
.height(LV_SIZE_CONTENT)
.bgOpa(0)
.border(0)
.padAll(0)
.scrollable(false)
.flexFlow(LV_FLEX_FLOW_ROW)
.flexAlign(
is_self ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START
);
lv_obj_set_style_pad_row(aligner, 4, 0);
// 3. Το Label-Bubble (Ενοποιημένο)
lv_obj_t *lbl_msg = lv_label_create(aligner);
// Long mode για wrap
lv_label_set_long_mode(lbl_msg, LV_LABEL_LONG_WRAP);
lv_label_set_text(lbl_msg, msg);
lv_obj_set_style_text_font(lbl_msg, &lv_font_arial_22, 0);
// Fixed max width για wrap
lv_obj_set_width(lbl_msg, 400); // max πλάτος
lv_obj_set_height(lbl_msg, LV_SIZE_CONTENT); // αυτόματο ύψος
// Bubble style
lv_obj_set_style_bg_opa(lbl_msg, LV_OPA_COVER, 0);
lv_obj_set_style_radius(lbl_msg, 12, 0);
lv_obj_set_style_pad_all(lbl_msg, 12, 0);
if(is_self) {
lv_obj_set_style_bg_color(lbl_msg, lv_color_hex(0x1E88E5), 0);
lv_obj_set_style_text_color(lbl_msg, lv_color_hex(0xFFFFFF), 0);
} else {
lv_obj_set_style_bg_color(lbl_msg, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_color(lbl_msg, lv_color_hex(0x000000), 0);
}
// 4. Η Ώρα
lv_obj_t *lbl_time = lv_label_create(aligner);
lv_label_set_text(lbl_time, time_str);
lv_obj_set_style_text_color(lbl_time, lv_color_hex(0x808080), 0);
lv_obj_set_style_text_font(lbl_time, &lv_font_arial_14, 0);
lv_obj_scroll_to_view(row, LV_ANIM_ON);
}
void UIManager::getInitials(const char *name, char *out) {
out[0] = 0;
if (!name || !name[0]) return;
const char *p = name;
while (*p && !isalnum((unsigned char)*p)) {
p++;
}
char first = (*p) ? *p : name[0];
char second = 0;
const char *space = strchr(name, ' ');
if (space) {
const char *s = space + 1;
while (*s && !isalnum((unsigned char)*s)) {
s++;
}
if (*s) {
second = *s;
}
}
out[0] = toupper((unsigned char)first);
if (second) {
out[1] = toupper((unsigned char)second);
out[2] = 0;
} else {
out[1] = 0;
}
}
void UIManager::formatLastSeen(uint32_t ts, char *out, size_t len) {
if (ts == 0) {
snprintf(out, len, "Never");
return;
}
time_t t = (time_t)ts;
struct tm *tm = localtime(&t);
if (tm == nullptr) {
snprintf(out, len, "Unknown");
return;
}
snprintf(out, len, "%02d:%02d %02d/%02d/%02d",
tm->tm_hour,
tm->tm_min,
tm->tm_mday,
tm->tm_mon + 1,
(tm->tm_year + 1900) % 100); // Προσθέτουμε το 1900 και παίρνουμε τα τελευταία 2 ψηφία
}
static void onContactClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->handleContactClick(e);
}
void UIManager::handleContactClick(lv_event_t *e)
{
lv_obj_t *row = lv_event_get_target(e);
ContactInfo *c = (ContactInfo*) lv_obj_get_user_data(row);
Serial.printf("Clicked: %s\n", c->name);
}
void UIManager::addContactToUI(ContactInfo c)
{
const int ROW_W = 200;
const int ROW_H = 64;
const int AVATAR = 44;
const int PAD = 4;
// ============================
// List row button
// ============================
lv_obj_t* btn = lv_list_add_btn(ui_Contacts, nullptr, nullptr);
LvObj(btn, true)
.size(ROW_W, ROW_H)
.padAll(0)
.bgColor(0x000000)
.bgOpa(LV_OPA_COVER)
.noDecor()
.border(1)
.scrollable(false);
lv_obj_set_layout(btn, 0);
lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0x222222), 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x111111), LV_STATE_PRESSED);
// Store ContactInfo
auto* store = new ContactInfo(c);
lv_obj_set_user_data(btn, store);
lv_obj_add_event_cb(btn, onContactClick, LV_EVENT_CLICKED, this);
// ============================
// Avatar container (hidden when SHOW_CONTACT_AVATAR is false)
// ============================
#ifdef SHOW_CONTACT_AVATAR
lv_obj_t* content = LvObj(btn)
.size(ROW_H, ROW_H)
.position(0, 0)
.padAll(0)
.bgOpa(LV_OPA_TRANSP)
.border(0)
.scrollable(false)
.raw();
// Avatar circle
lv_obj_t* avatar = LvObj(content)
.size(AVATAR, AVATAR)
.position(0, 10)
.radius(LV_RADIUS_CIRCLE)
.bgColor(0x4A90E2)
.border(0)
.raw();
// Initials
char initials[4];
if (c.type == ADV_TYPE_REPEATER) {
strcpy(initials, "(R)");
} else {
getInitials(c.name, initials);
}
LvLabel(avatar)
.text(initials)
.font(&lv_font_arial_22)
.textColor(0xFFFFFF)
.align(LV_ALIGN_CENTER);
#endif
// ============================
// Text column
// ============================
#ifdef SHOW_CONTACT_AVATAR
int text_x = PAD + AVATAR + PAD;
#else
int text_x = 0;
#endif
int text_w = ROW_W - text_x - PAD;
lv_obj_t* text_col = LvObj(btn)
.position(text_x, 0)
.size(text_w, ROW_H - PAD)
.padAll(0)
.bgOpa(LV_OPA_TRANSP)
.border(0)
.scrollable(false)
.raw();
// Name label
LvLabel(text_col)
.text(c.name)
.position(0, PAD + 4)
.width(text_w)
.font(&lv_font_arial_20)
.textColor(0xFFFFFF)
.wrap(false);
// Last seen
char lastSeen[32];
formatLastSeen(c.lastmod, lastSeen, sizeof(lastSeen));
LvLabel(text_col)
.text(lastSeen)
.position(0, PAD + 32)
.width(text_w)
.font(&lv_font_arial_16)
.textColor(0x888888)
.wrap(false);
// ============================
// Disable child clicks
// ============================
#ifdef SHOW_CONTACT_AVATAR
LvObj(avatar, true).clickable(false);
#endif
LvObj(text_col, true).clickable(false);
}
void UIManager::onShowKeyboard()
{
LvKeyboard(ui_Keyboard, true).show(true);
LvObj(ui_DimOverlay, true).clickable(true);
LvObj(ui_ChannelInput, true).positionY(channelInputBaseKeybOnY);
LvObj(ui_SendBtn, true).positionY(channelInputBaseKeybOnY);
}
void UIManager::onHideKeyboard()
{
LvKeyboard(ui_Keyboard, true).show(false);
LvObj(ui_DimOverlay, true).clickable(false);
LvObj(ui_ChannelInput, true).positionY(channelInputBaseY);
LvObj(ui_SendBtn, true).positionY(channelInputBaseY);
}
static void s_onChannelInputFocus(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onChannelInputFocus(e);
}
void UIManager::onChannelInputFocus(lv_event_t* e)
{
lv_obj_t* ta = lv_event_get_target(e);
if(!ui_Keyboard || !ta) return;
lv_keyboard_set_textarea(ui_Keyboard, ta);
onShowKeyboard();
}
static void s_onSendClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onSendClick(e);
}
void UIManager::onSendClick(lv_event_t* e)
{
char fullMessage[260];
char msgCopy[200];
const char* msg = lv_textarea_get_text(ui_ChannelInput);
if(msg == NULL || msg[0] == '\0') return;
strncpy(msgCopy, msg, sizeof(msgCopy) - 1);
msgCopy[sizeof(msgCopy) - 1] = '\0';
lv_textarea_set_text(ui_ChannelInput, "");
snprintf(fullMessage, sizeof(fullMessage), "public %s", msgCopy);
//handleCommand(fullMessage);
handleCommand(msgCopy);
char time_buf[16];
time_t now = time(NULL);
struct tm t;
localtime_r(&now, &t);
sprintf(time_buf, "%02d:%02d:%02d\n", t.tm_hour, t.tm_min, t.tm_sec);
addChatBubble(time_buf, "Me", msgCopy, true);
onHideKeyboard();
}
static void s_onKeyboardEvent(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onKeyboardEvent(e);
}
void UIManager::onKeyboardEvent(lv_event_t* e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL)
{
LvKeyboard(ui_Keyboard, true).show(false);
LvObj(ui_ChannelInput, true).positionY(channelInputBaseY);
LvObj(ui_SendBtn, true).positionY(channelInputBaseY);
LvObj(ui_DimOverlay, true)
.bgOpa(0)
.clickable(false);
}
}
static void s_onDimOverlayClick(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->onDimOverlayClick(e);
}
void UIManager::onDimOverlayClick(lv_event_t* e)
{
onHideKeyboard();
}
static void onScrollBeginEvent(lv_event_t *e)
{
UIManager *self = (UIManager*) lv_event_get_user_data(e);
if(self) self->scroll_begin_event(e);
}
void UIManager::scroll_begin_event(lv_event_t *e)
{
if (lv_event_get_code(e) == LV_EVENT_SCROLL_BEGIN) {
lv_anim_t* a = (lv_anim_t*)lv_event_get_param(e);
if (a) a->time = 0;
}
}
void UIManager::ui_Screen1_screen_init(void)
{
//lv_disp_set_rotation(disp, LV_DISP_ROT_90);
//ui_Screen1 = lv_obj_create(NULL);
ui_Screen1 = LvObj(NULL)
.scrollable(false)
.bgColor(0x000000)
.bgOpa(255);
LvTabView tabView(ui_Screen1);
tabView
.size(480, 480)
.align(LV_ALIGN_CENTER)
.bgColor(0x000000)
.contentNoScroll()
.tabBtnBg(0x424242)
.tabBtnText(0xFFFFFF, &lv_font_arial_18);
ui_TabView1 = tabView.raw();
#if defined(LANG_EN)
ui_TabPageHome = tabView.addTab("Home");
ui_TabPageContacts = tabView.addTab("Contacts");
ui_TabPageChannels = tabView.addTab("Channels");
ui_TabPageSettings = tabView.addTab("Settings");
#elif defined(LANG_GR)
ui_TabPageHome = tabView.addTab("Αρχική");
ui_TabPageContacts = tabView.addTab("Επαφές");
ui_TabPageChannels = tabView.addTab("Κανάλια");
ui_TabPageSettings = tabView.addTab("Ρυθμίσεις");
#endif
LvObj(ui_TabPageHome)
.scrollable(false)
.bgOpa(0)
.bgColor(0x000000);
LvObj(ui_TabPageContacts)
.scrollable(false)
.bgOpa(0)
.bgColor(0x000000);
LvObj(ui_TabPageChannels)
.scrollable(false)
.bgOpa(0)
.bgColor(0x000000);
LvObj(ui_TabPageSettings)
.scrollable(false)
.bgOpa(0)
.bgColor(0x000000);
ui_ValueDate = LvLabel(ui_TabPageHome)
.text("--- --/--/----")
.width(LV_SIZE_CONTENT)
.height(LV_SIZE_CONTENT)
.font(&lv_font_arial_40)
.textColor(0xFFFFFF)
.opa(255)
.align(LV_ALIGN_CENTER)
.position(0, -165);
ui_ValueTime = LvLabel(ui_TabPageHome)
.text("--:--")
.width(LV_SIZE_CONTENT)
.height(LV_SIZE_CONTENT)
.font(&lv_font_arial_48)
.textColor(0xFFFFFF)
.opa(255)
.align(LV_ALIGN_CENTER)
.position(0, -100);
ui_Contacts = LvList(ui_TabPageContacts)
.width(250)
.height(400)
.align(LV_ALIGN_CENTER)
.position(-274, 0)
.transparent()
.raw();
lv_obj_set_style_bg_opa(ui_Contacts, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(ui_Contacts, 0, 0);
lv_obj_set_style_outline_width(ui_Contacts, 0, 0);
lv_obj_set_style_shadow_width(ui_Contacts, 0, 0);
//lv_obj_set_scrollbar_mode(ui_Contacts, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_bg_opa(ui_Contacts, LV_OPA_TRANSP, LV_PART_ITEMS);
lv_obj_set_style_border_width(ui_Contacts, 0, LV_PART_ITEMS);
ui_ContactMessages = LvList(ui_TabPageContacts)
.width(500)
.height(400)
.align(LV_ALIGN_CENTER)
.position(124, 0)
.transparent()
.raw();
lv_obj_set_style_bg_color(ui_ContactMessages, lv_color_hex(0), 0);
lv_obj_set_style_bg_opa(ui_ContactMessages, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(ui_ContactMessages, 0, 0);
lv_obj_set_style_outline_width(ui_ContactMessages, 0, 0);
lv_obj_set_style_shadow_width(ui_ContactMessages, 0, 0);
//lv_obj_set_scrollbar_mode(ui_ContactMessages, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_bg_opa(ui_ContactMessages, LV_OPA_TRANSP, LV_PART_ITEMS);
lv_obj_set_style_border_width(ui_ContactMessages, 0, LV_PART_ITEMS);
// LvObj(ui_TabPageContacts)
// .size(2, 400)
// .position(222, 0)
// .bgColor(0x444444)
// .border(0)
// .scrollable(false)
// .radius(0);
ui_Channels = LvDropdown(ui_TabPageChannels)
.options("Public")
.width(291)
.align(LV_ALIGN_CENTER)
.position(-243, -182)
.clickable(true)
.raw();
ui_ChannelMessages = LvList(ui_TabPageChannels)
.width(780)
.height(280)
.align(LV_ALIGN_CENTER)
.transparent()
.padRow(10)
.position(0, 0)
.bgColor(0)
.bgOpa(0)
.border(0)
.noDecor()
.raw();
//lv_obj_set_scrollbar_mode(ui_ChannelMessages, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_bg_opa(ui_ChannelMessages, LV_OPA_TRANSP, LV_PART_ITEMS);
lv_obj_set_style_border_width(ui_ChannelMessages, 0, LV_PART_ITEMS);
ui_ChannelDivider = LvObj(ui_TabPageChannels)
.size(780, 1)
.align(LV_ALIGN_CENTER)
.position(0, 150)
.bgColor(0x444444)
.border(0)
.raw();
ui_DimOverlay = LvObj(ui_Screen1)
.size(lv_pct(100), lv_pct(100))
.align(LV_ALIGN_CENTER)
.bgColor(0x000000)
.bgOpa(0)
.bringToFront()
.onClick(s_onDimOverlayClick, this)
.scrollable(false)
.clickable(true)
.raw();
lv_obj_remove_style_all(ui_DimOverlay); // no border/padding
lv_obj_clear_flag(ui_DimOverlay, LV_OBJ_FLAG_SCROLL_CHAIN_HOR);
lv_obj_clear_flag(ui_DimOverlay, LV_OBJ_FLAG_SCROLL_CHAIN_VER);
ui_ChannelInput = LvTextArea(ui_TabPageChannels)
.size(670, 40)
.align(LV_ALIGN_CENTER)
.position(-50, channelInputBaseY)
.oneLine(true)
#if defined(LANG_EN)
.placeholder("Write message...")
#elif defined(LANG_GR)
.placeholder("Γράψε μήνυμα...")
#endif
.font(&lv_font_arial_20)
.bgColor(0x111111)
.textColor(0xFFFFFF)
.borderColor(0x444444)
.borderWidth(1)
.radius(6)
.onFocus(s_onChannelInputFocus, this)
.raw();
ui_SendBtn = LvButton(ui_TabPageChannels)
.size(90, 42)
.align(LV_ALIGN_CENTER)
.position(350, channelInputBaseY)
.bgColor(0x3A7AFE)
.onClick(s_onSendClick, this)
.raw();
iu_SendLabel = LvLabel(ui_SendBtn)
#if defined(LANG_EN)
.text("Send")
#elif defined(LANG_GR)
.text("Αποστολή")
#endif
.font(&lv_font_arial_18);
lv_obj_center(iu_SendLabel);
ui_Keyboard = LvKeyboard(lv_layer_top())
.size(480, 200)
.align(LV_ALIGN_BOTTOM_MID)
.show(false)
.onEvent(s_onKeyboardEvent, this);
}
void UIManager::setNightMode(bool night) {
if (!ui_DimOverlay) return;
if (night) {
lv_obj_set_style_bg_opa(ui_DimOverlay, 192, 0); // 75% dark
} else {
lv_obj_set_style_bg_opa(ui_DimOverlay, 0, 0); // none
}
}

50
examples/companion_radio/uiTasks.cpp

@ -0,0 +1,50 @@
#include <Arduino.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiTasks.h"
#include "uiVars.h"
// Tasks
TaskHandle_t t_core0_lvgl;
TaskHandle_t t_core1_clock;
TaskHandle_t t_core1_core;
#define TAG "createTasks"
void createTasks() {
Serial.println("Creating Tasks...");
xTaskCreatePinnedToCore(
lvgl_task, // Task function.
"LVGL_Manager", // Name of task.
10000, // Stack size of task
NULL, // Parameter of the task
5, // Priority of the task
&t_core0_lvgl, // Task handle to keep track of created task
0); // Pin task to core 0
xTaskCreatePinnedToCore(
core_task, // Task function.
"MeshCore", // Name of task.
10000, // Stack size of task
NULL, // Parameter of the task
4, // Priority of the task
&t_core1_core, // Task handle to keep track of created task
1); // Pin task to core 1
xTaskCreatePinnedToCore(
clock_task, // Task function.
"CLOCK_Manager", // Name of task.
10000, // Stack size of task
NULL, // Parameter of the task
4, // Priority of the task
&t_core1_clock, // Task handle to keep track of created task
1); // Pin task to core 1
ESP_LOGD(TAG, "All tasks created\nStarting tasks...");
vTaskResume(t_core0_lvgl);
vTaskResume(t_core1_clock);
}

10
examples/simple_secure_chat/main.cpp

@ -158,13 +158,13 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
}
void setClock(uint32_t timestamp) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (timestamp > curr) {
//uint32_t curr = getRTCClock()->getCurrentTime();
//if (timestamp > curr) {
getRTCClock()->setCurrentTime(timestamp);
Serial.println(" (OK - clock set!)");
} else {
Serial.println(" (ERR: clock cannot go backwards)");
}
//} else {
// Serial.println(" (ERR: clock cannot go backwards)");
//}
}
void importCard(const char* command) {

1265
examples/simple_secure_chat_ui/main.cpp

File diff suppressed because it is too large

15
examples/simple_secure_chat_ui/messageStore.h

@ -0,0 +1,15 @@
#pragma once
#include <stdint.h>
// Append-only per-contact / public chat persistence on SPIFFS.
// Each DM contact gets a file keyed by the first 4 bytes of the public key.
// When a file exceeds MSGSTORE_MAX_BYTES it is dropped (oldest history lost).
#define MSGSTORE_MAX_BYTES 16384
void msgstore_append_dm(const uint8_t* pub_key, uint32_t ts, bool sent, const char* text);
void msgstore_append_public(uint32_t ts, const char* sender, bool sent, const char* text);
// Replays stored history into the UI (calls uiManager->addPrivateChatBubble / addChatBubble).
void msgstore_load_dm(const uint8_t* pub_key);
void msgstore_load_public();

25
examples/simple_secure_chat_ui/task_clock.cpp

@ -0,0 +1,25 @@
#include <Arduino.h>
#include <time.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiVars.h"
#define TAG "clock_task"
void clock_task(void *pvParameters) {
ESP_LOGI(TAG, "Clock manager: Task running on core %d", xPortGetCoreID());
uiManager->clearDateTime();
while (1) {
if (g_clock_synced) {
time_t now = time(nullptr);
struct tm t;
localtime_r(&now, &t);
uiManager->updateDateTime(t);
}
vTaskDelay(DELAY_CLOCK_TASK / portTICK_PERIOD_MS);
}
}

19
examples/simple_secure_chat_ui/task_lvgl.cpp

@ -0,0 +1,19 @@
#include <Arduino.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiVars.h"
#define TAG "lvgl_task"
void lvgl_task(void *pvParameters) {
vTaskSuspend(NULL);
ESP_LOGI(TAG, "UI manager: Task running on core %d", xPortGetCoreID());
while (1) {
lv_timer_handler();
vTaskDelay(DELAY_LVGL_TASK / portTICK_PERIOD_MS);
}
}

1656
examples/simple_secure_chat_ui/uiManager.cpp

File diff suppressed because it is too large

50
examples/simple_secure_chat_ui/uiTasks.cpp

@ -0,0 +1,50 @@
#include <Arduino.h>
#include "esp_log.h"
#include "uiDefines.h"
#include "uiTasks.h"
#include "uiVars.h"
// Tasks
TaskHandle_t t_core0_lvgl;
TaskHandle_t t_core1_clock;
TaskHandle_t t_core1_core;
#define TAG "createTasks"
void createTasks() {
Serial.println("Creating Tasks...");
xTaskCreatePinnedToCore(
lvgl_task, // Task function.
"LVGL_Manager", // Name of task.
10000, // Stack size of task
NULL, // Parameter of the task
5, // Priority of the task
&t_core0_lvgl, // Task handle to keep track of created task
0); // Pin task to core 0
xTaskCreatePinnedToCore(
core_task, // Task function.
"MeshCore", // Name of task.
10000, // Stack size of task
NULL, // Parameter of the task
4, // Priority of the task
&t_core1_core, // Task handle to keep track of created task
1); // Pin task to core 1
xTaskCreatePinnedToCore(
clock_task, // Task function.
"CLOCK_Manager", // Name of task.
10000, // Stack size of task
NULL, // Parameter of the task
4, // Priority of the task
&t_core1_clock, // Task handle to keep track of created task
1); // Pin task to core 1
ESP_LOGD(TAG, "All tasks created\nStarting tasks...");
vTaskResume(t_core0_lvgl);
vTaskResume(t_core1_clock);
}

217
include/lgfx.h

@ -0,0 +1,217 @@
#ifndef LGFX_h
#define LGFX_h
#include <Arduino.h>
#include <LovyanGFX.hpp>
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
#ifndef SEEED_SENSECAP_INDICATOR
class LGFX : public lgfx::LGFX_Device
{
public:
lgfx::Bus_RGB _bus_instance;
lgfx::Panel_RGB _panel_instance;
#if defined(ELECROW_DISPLAY_35)
LGFX(void)
{
{
auto cfg = _bus_instance.config();
cfg.port = 0;
cfg.freq_write = 80000000;
cfg.pin_wr = GPIO_NUM_18;
cfg.pin_rd = GPIO_NUM_48;
cfg.pin_rs = GPIO_NUM_45;
cfg.pin_d0 = GPIO_NUM_47;
cfg.pin_d1 = GPIO_NUM_21;
cfg.pin_d2 = GPIO_NUM_14;
cfg.pin_d3 = GPIO_NUM_13;
cfg.pin_d4 = GPIO_NUM_12;
cfg.pin_d5 = GPIO_NUM_11;
cfg.pin_d6 = GPIO_NUM_10;
cfg.pin_d7 = GPIO_NUM_9;
cfg.pin_d8 = GPIO_NUM_3;
cfg.pin_d9 = GPIO_NUM_8;
cfg.pin_d10 = GPIO_NUM_16;
cfg.pin_d11 = GPIO_NUM_15;
cfg.pin_d12 = GPIO_NUM_7;
cfg.pin_d13 = GPIO_NUM_6;
cfg.pin_d14 = GPIO_NUM_5;
cfg.pin_d15 = GPIO_NUM_4;
_bus_instance.config(cfg);
_panel_instance.setBus(&_bus_instance);
}
{
auto cfg = _panel_instance.config();
cfg.pin_cs = -1;
cfg.pin_rst = -1;
cfg.pin_busy = -1;
cfg.memory_width = 320;
cfg.memory_height = 480;
cfg.panel_width = 320;
cfg.panel_height = 480;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 0;
cfg.dummy_read_pixel = 8;
cfg.dummy_read_bits = 1;
cfg.readable = true;
cfg.invert = false;
cfg.rgb_order = false;
cfg.dlen_16bit = true;
cfg.bus_shared = true;
_panel_instance.config(cfg);
}
setPanel(&_panel_instance);
}
#elif defined(ELECROW_DISPLAY_50)
LGFX(void)
{
{
auto cfg = _bus_instance.config();
cfg.panel = &_panel_instance;
cfg.pin_d0 = GPIO_NUM_8; // B0
cfg.pin_d1 = GPIO_NUM_3; // B1
cfg.pin_d2 = GPIO_NUM_46; // B2
cfg.pin_d3 = GPIO_NUM_9; // B3
cfg.pin_d4 = GPIO_NUM_1; // B4
cfg.pin_d5 = GPIO_NUM_5; // G0
cfg.pin_d6 = GPIO_NUM_6; // G1
cfg.pin_d7 = GPIO_NUM_7; // G2
cfg.pin_d8 = GPIO_NUM_15; // G3
cfg.pin_d9 = GPIO_NUM_16; // G4
cfg.pin_d10 = GPIO_NUM_4; // G5
cfg.pin_d11 = GPIO_NUM_45; // R0
cfg.pin_d12 = GPIO_NUM_48; // R1
cfg.pin_d13 = GPIO_NUM_47; // R2
cfg.pin_d14 = GPIO_NUM_21; // R3
cfg.pin_d15 = GPIO_NUM_14; // R4
cfg.pin_henable = GPIO_NUM_40;
cfg.pin_vsync = GPIO_NUM_41;
cfg.pin_hsync = GPIO_NUM_39;
cfg.pin_pclk = GPIO_NUM_0;
cfg.freq_write = 15000000;
cfg.hsync_polarity = 0;
cfg.hsync_front_porch = 8;
cfg.hsync_pulse_width = 4;
cfg.hsync_back_porch = 43;
cfg.vsync_polarity = 0;
cfg.vsync_front_porch = 8;
cfg.vsync_pulse_width = 4;
cfg.vsync_back_porch = 12;
cfg.pclk_active_neg = 1;
cfg.de_idle_high = 0;
cfg.pclk_idle_high = 0;
_bus_instance.config(cfg);
}
{
auto cfg = _panel_instance.config();
cfg.memory_width = 800;
cfg.memory_height = 480;
cfg.panel_width = 800;
cfg.panel_height = 480;
cfg.offset_x = 0;
cfg.offset_y = 0;
_panel_instance.config(cfg);
}
_panel_instance.setBus(&_bus_instance);
setPanel(&_panel_instance);
}
#elif defined(ELECROW_DISPLAY_70)
// Constructor for the LGFX class.
LGFX(void) {
// Configure the RGB bus.
{
auto cfg = _bus_instance.config();
cfg.panel = &_panel_instance;
// Configure data pins.
cfg.pin_d0 = GPIO_NUM_15; // B0
cfg.pin_d1 = GPIO_NUM_7; // B1
cfg.pin_d2 = GPIO_NUM_6; // B2
cfg.pin_d3 = GPIO_NUM_5; // B3
cfg.pin_d4 = GPIO_NUM_4; // B4
cfg.pin_d5 = GPIO_NUM_9; // G0
cfg.pin_d6 = GPIO_NUM_46; // G1
cfg.pin_d7 = GPIO_NUM_3; // G2
cfg.pin_d8 = GPIO_NUM_8; // G3
cfg.pin_d9 = GPIO_NUM_16; // G4
cfg.pin_d10 = GPIO_NUM_1; // G5
cfg.pin_d11 = GPIO_NUM_14; // R0
cfg.pin_d12 = GPIO_NUM_21; // R1
cfg.pin_d13 = GPIO_NUM_47; // R2
cfg.pin_d14 = GPIO_NUM_48; // R3
cfg.pin_d15 = GPIO_NUM_45; // R4
// Configure sync and clock pins.
cfg.pin_henable = GPIO_NUM_41;
cfg.pin_vsync = GPIO_NUM_40;
cfg.pin_hsync = GPIO_NUM_39;
cfg.pin_pclk = GPIO_NUM_0;
cfg.freq_write = 15000000;
// Configure timing parameters for horizontal and vertical sync.
cfg.hsync_polarity = 0;
cfg.hsync_front_porch = 40;
cfg.hsync_pulse_width = 48;
cfg.hsync_back_porch = 40;
cfg.vsync_polarity = 0;
cfg.vsync_front_porch = 1;
cfg.vsync_pulse_width = 31;
cfg.vsync_back_porch = 13;
// Configure polarity for clock and data transmission.
cfg.pclk_active_neg = 1;
cfg.de_idle_high = 0;
cfg.pclk_idle_high = 0;
// Apply configuration to the RGB bus instance.
_bus_instance.config(cfg);
}
// Configure the panel.
{
auto cfg = _panel_instance.config();
cfg.memory_width = 800;
cfg.memory_height = 480;
cfg.panel_width = 800;
cfg.panel_height = 480;
cfg.offset_x = 0;
cfg.offset_y = 0;
// Apply configuration to the panel instance.
_panel_instance.config(cfg);
}
// Set the RGB bus and panel instances.
_panel_instance.setBus(&_bus_instance);
setPanel(&_panel_instance);
}
#elif defined(SEEED_SENSECAP_INDICATOR)
#else
#error "No Display size defined!"
#endif
};
#endif
#endif

107
include/lvBase.h

@ -0,0 +1,107 @@
#ifndef LV_BASE_h
#define LV_BASE_h
#include "lvgl.h"
template<typename T>
class LvBase {
public:
operator lv_obj_t*() const { return obj; }
lv_obj_t* raw() const { return obj; }
T& width(int v) {
lv_obj_set_width(obj, v == 100 ? lv_pct(100) : v);
return self();
}
T& height(int v) {
lv_obj_set_height(obj, v);
return self();
}
T& padAll(int v) {
lv_obj_set_style_pad_all(obj, v, 0);
return self();
}
T& bgOpa(int v) {
lv_obj_set_style_bg_opa(obj, v, 0);
return self();
}
T& border(int w) {
lv_obj_set_style_border_width(obj, w, 0);
return self();
}
T& scrollable(bool isScrollable) {
if (isScrollable) {
lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_AUTO);
} else {
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_OFF);
}
return self();
}
T& position(lv_coord_t x, lv_coord_t y) {
lv_obj_set_x(obj, x);
lv_obj_set_y(obj, y);
return self();
}
T& positionY(lv_coord_t y) {
lv_obj_set_y(obj, y);
return self();
}
T& positionX(lv_coord_t x) {
lv_obj_set_x(obj, x);
return self();
}
T& size(int w, int h) {
lv_obj_set_size(obj, w, h);
return self();
}
T& align(lv_align_t align) {
lv_obj_set_align(obj, align);
return self();
}
T& bgColor(uint32_t hex) {
lv_obj_set_style_bg_color(obj, lv_color_hex(hex), 0);
return self();
}
T& noDecor() {
lv_obj_set_style_outline_width(obj, 0, 0);
lv_obj_set_style_shadow_width(obj, 0, 0);
return self();
}
T& clickable(bool isClickable) {
if (isClickable) {
lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE);
} else {
lv_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE);
}
return self();
}
T& onClick(lv_event_cb_t cb, void* user = nullptr) {
lv_obj_add_event_cb(obj, cb, LV_EVENT_CLICKED, user);
return self();
}
protected:
lv_obj_t* obj = nullptr;
private:
T& self() {
return static_cast<T&>(*this);
}
};
#endif

12
include/lvButton.h

@ -0,0 +1,12 @@
#ifndef LV_BUTTON_h
#define LV_BUTTON_h
#include "lvBase.h"
class LvButton : public LvBase<LvButton> {
public:
explicit LvButton(lv_obj_t* parent) {
obj = lv_btn_create(parent);
}
};
#endif

23
include/lvDropDown.h

@ -0,0 +1,23 @@
#ifndef LV_DROP_DOWN_h
#define LV_DROP_DOWN_h
#include "lvBase.h"
class LvDropdown : public LvBase<LvDropdown> {
public:
explicit LvDropdown(lv_obj_t* parent) {
obj = lv_dropdown_create(parent);
}
LvDropdown& options(const char* opts) {
lv_dropdown_set_options(obj, opts);
return *this;
}
LvDropdown& selected(uint16_t idx) {
lv_dropdown_set_selected(obj, idx);
return *this;
}
};
#endif

32
include/lvKeyboard.h

@ -0,0 +1,32 @@
#ifndef LV_KEYBOARD_h
#define LV_KEYBOARD_h
#include "lvBase.h"
class LvKeyboard : public LvBase<LvKeyboard> {
public:
explicit LvKeyboard(lv_obj_t* parent) {
obj = lv_keyboard_create(parent);
}
explicit LvKeyboard(lv_obj_t* existing, bool) {
obj = existing;
}
LvKeyboard& target(lv_obj_t* ta) {
lv_keyboard_set_textarea(obj, ta);
return *this;
}
LvKeyboard& show(bool v = true) {
v ? lv_obj_clear_flag(obj, LV_OBJ_FLAG_HIDDEN)
: lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
return *this;
}
LvKeyboard& onEvent(lv_event_cb_t cb, void* user = nullptr) {
lv_obj_add_event_cb(obj, cb, LV_EVENT_ALL, user);
return *this;
}
};
#endif

41
include/lvLabel.h

@ -0,0 +1,41 @@
#ifndef LV_LABEL_h
#define LV_LABEL_h
#include "lvBase.h"
class LvLabel : public LvBase<LvLabel> {
public:
explicit LvLabel(lv_obj_t* parent) {
obj = lv_label_create(parent);
}
LvLabel& text(const char* t) {
lv_label_set_text(obj, t);
return *this;
}
LvLabel& font(const lv_font_t* f) {
lv_obj_set_style_text_font(obj, f, 0);
return *this;
}
LvLabel& textColor(uint32_t hex) {
lv_obj_set_style_text_color(obj, lv_color_hex(hex), 0);
return *this;
}
LvLabel& opa(int v) {
lv_obj_set_style_text_opa(obj, v, 0);
return *this;
}
LvLabel& wrap(bool v = true) {
lv_label_set_long_mode(
obj,
v ? LV_LABEL_LONG_WRAP : LV_LABEL_LONG_CLIP
);
return *this;
}
};
#endif

28
include/lvList.h

@ -0,0 +1,28 @@
#ifndef LV_LIST_h
#define LV_LIST_h
#include "lvBase.h"
class LvList : public LvBase<LvList> {
public:
explicit LvList(lv_obj_t* parent) {
obj = lv_list_create(parent);
}
LvList& transparent() {
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, LV_PART_ITEMS);
lv_obj_set_style_border_width(obj, 0, LV_PART_ITEMS);
return *this;
}
LvList& padRow(int v) {
lv_obj_set_style_pad_row(obj, v, 0);
return *this;
}
};
#endif

42
include/lvObj.h

@ -0,0 +1,42 @@
#ifndef LV_OBJ_h
#define LV_OBJ_h
#include "lvBase.h"
class LvObj : public LvBase<LvObj> {
public:
explicit LvObj(lv_obj_t* parent) {
obj = lv_obj_create(parent);
}
explicit LvObj(lv_obj_t* existing, bool) {
obj = existing;
}
LvObj& flexFlow(lv_flex_flow_t flow) {
lv_obj_set_flex_flow(obj, flow);
return *this;
}
LvObj& flexAlign(
lv_flex_align_t main,
lv_flex_align_t cross,
lv_flex_align_t track = LV_FLEX_ALIGN_START
) {
lv_obj_set_flex_align(obj, main, cross, track);
return *this;
}
LvObj& bringToFront() {
lv_obj_move_foreground(obj);
return *this;
}
LvObj& radius(int r) {
lv_obj_set_style_radius(obj, r, 0);
return *this;
}
};
#endif

51
include/lvTabView.h

@ -0,0 +1,51 @@
#ifndef LV_TAB_VIEW_h
#define LV_TAB_VIEW_h
#include "lvBase.h"
class LvTabView : public LvBase<LvTabView> {
public:
explicit LvTabView(lv_obj_t* parent,
lv_dir_t dir = LV_DIR_TOP,
int tab_h = 50) {
obj = lv_tabview_create(parent, dir, tab_h);
}
lv_obj_t* addTab(const char* name) {
return lv_tabview_add_tab(obj, name);
}
LvTabView& contentNoScroll() {
lv_obj_clear_flag(
lv_tabview_get_content(obj),
LV_OBJ_FLAG_SCROLLABLE
);
return *this;
}
LvTabView& tabBtnBg(uint32_t hex) {
lv_obj_set_style_bg_color(
lv_tabview_get_tab_btns(obj),
lv_color_hex(hex),
0
);
return *this;
}
LvTabView& tabBtnText(uint32_t hex, const lv_font_t* f = nullptr) {
lv_obj_set_style_text_color(
lv_tabview_get_tab_btns(obj),
lv_color_hex(hex),
LV_PART_ITEMS
);
if (f) {
lv_obj_set_style_text_font(
lv_tabview_get_tab_btns(obj),
f,
LV_PART_ITEMS
);
}
return *this;
}
};
#endif

52
include/lvTextArea.h

@ -0,0 +1,52 @@
#ifndef LV_TEXT_AREA_h
#define LV_TEXT_AREA_h
#include "lvBase.h"
class LvTextArea : public LvBase<LvTextArea> {
public:
explicit LvTextArea(lv_obj_t* parent) {
obj = lv_textarea_create(parent);
}
LvTextArea& oneLine(bool v = true) {
lv_textarea_set_one_line(obj, v);
return *this;
}
LvTextArea& placeholder(const char* txt) {
lv_textarea_set_placeholder_text(obj, txt);
return *this;
}
LvTextArea& font(const lv_font_t* f) {
lv_obj_set_style_text_font(obj, f, 0);
return *this;
}
LvTextArea& textColor(uint32_t hex) {
lv_obj_set_style_text_color(obj, lv_color_hex(hex), 0);
return *this;
}
LvTextArea& borderColor(uint32_t hex) {
lv_obj_set_style_border_color(obj, lv_color_hex(hex), 0);
return *this;
}
LvTextArea& borderWidth(int w) {
lv_obj_set_style_border_width(obj, w, 0);
return *this;
}
LvTextArea& radius(int r) {
lv_obj_set_style_radius(obj, r, 0);
return *this;
}
LvTextArea& onFocus(lv_event_cb_t cb, void* user = nullptr) {
lv_obj_add_event_cb(obj, cb, LV_EVENT_CLICKED, user);
return *this;
}
};
#endif

61
include/uiDefines.h

@ -0,0 +1,61 @@
#ifndef DEFINES_h
#define DEFINES_h
#define BUTTONS_ON_SCREEN 7
#define DELAY_CORE_TASK 5
#define DELAY_LVGL_TASK 10
#define DELAY_MAIN_TASK 500
#define DELAY_CLOCK_TASK 1000
#define DELAY_NTP_TASK 30000
#define DELAY_WIFI_RECONNECT_TASK 5000
// every 5 minutes on success
#define DELAY_OPEN_WEATHER_TASK 300000
// every 1 minute on failure
#define DELAY_OPEN_WEATHER_SHORT_TASK 60000
#define STATE_OFF HIGH
#define STATE_ON LOW
#define MAX_CHAT_MESSAGES 50
// Colors
#define COLOR_WHITE 0xFFFFFF
#define COLOR_BLACK 0
#define COLOR_RED 0xFF0000
#define COLOR_BLUE 0x0077FF
#define COLOR_BLUE_LIGHT 0x22A8FD
#define COLOR_GREY 0x8F8F8F
#define BUTTON_BACKGROUND 0x1B1B1B
// Device Elecrow 5 & 7 inches
#define SD_MOSI 11
#define SD_MISO 13
#define SD_SCK 12
#define SD_CS 10
#define I2S_DOUT 17
#define I2S_BCLK 42
#define I2S_LRC 18
#define GPIO_D 38
#define UART_RX 44
#define UART_TX 43
#define I2C_SDA 19
#define I2C_SCL 20
#define LCD_Backlight 2
// Contact list: show avatar circle with initials (disable on small screens)
//#define SHOW_CONTACT_AVATAR
// Other defines
#define DELAY_MAIN_TASK 10
#define DELAY_REFRESH_VIEW 10
#define OLED_RESET -1
#define TFT_BL 2
#define LGFX_USE_V1
#endif

11
include/uiExternals.h

@ -0,0 +1,11 @@
#ifndef EXTERNALS_h
#define EXTERNALS_h
#include <Arduino.h>
extern void createTasks();
extern TaskHandle_t t_core0_lvgl;
extern TaskHandle_t t_core1_clock;
extern TaskHandle_t t_core1_core;
#endif

145
include/uiManager.h

@ -0,0 +1,145 @@
#ifndef UI_MANAGER_h
#define UI_MANAGER_h
#include <Arduino.h>
#include "uiDefines.h"
#include "lvButton.h"
#include "lvDropDown.h"
#include "lvKeyboard.h"
#include "lvLabel.h"
#include "lvList.h"
#include "lvObj.h"
#include "lvTabView.h"
#include "lvTextArea.h"
#include <helpers/ContactInfo.h>
class UIManager {
private:
// functions
void format_datetime(char *buf, size_t size, const struct tm *timeinfo);
void timestampToTime(time_t timestamp, char *buffer, size_t buffer_size);
const char* convertDegreesToDirection(int degrees);
int windSpeedToBeaufort(float speed);
void getInitials(const char *name, char *out);
void formatLastSeen(uint32_t ts, char *out, size_t len);
void format_time(uint32_t ts, char *buf, size_t len);
// Calendar days and months
static const char *days[7];
static const char *months[12];
// vars
char time_str[9];
String lastUpdate = "";
char* tmp_buf;
lv_obj_t *chat_items[MAX_CHAT_MESSAGES];
int chat_count = 0;
int channelInputBaseY = 185;
int channelInputBaseKeybOnY = -15;
void onShowKeyboard();
void onHideKeyboard();
void ui_Screen1_screen_init(void);
lv_obj_t* ui_Screen1;
lv_obj_t* ui_TabView1;
lv_obj_t* ui_TabPageContacts;
lv_obj_t* ui_Contacts;
lv_obj_t* ui_ContactMessages;
lv_obj_t* ui_TabPageChannels;
lv_obj_t* ui_Channels;
lv_obj_t* ui_ChannelMessages;
lv_obj_t* ui_AutoLight;
lv_obj_t* ui____initial_actions0;
lv_obj_t* ui_DimOverlay;
lv_obj_t* ui_TabPageHome;
lv_obj_t* ui_ValueDate;
lv_obj_t* ui_ValueTime;
lv_obj_t* ui_TabPageSettings;
lv_obj_t* ui_DayLight;
lv_obj_t* ui_ChannelInput;
lv_obj_t* ui_SendBtn;
lv_obj_t* ui_Keyboard;
lv_obj_t* iu_SendLabel;
lv_obj_t* ui_ChannelDivider;
lv_obj_t* ui_ContactName = nullptr;
lv_obj_t* ui_ContactInput = nullptr;
lv_obj_t* ui_ContactSendBtn = nullptr;
lv_obj_t* ui_ContactSendLabel = nullptr;
char currentContactName[32] = {0};
uint8_t currentContactPubKey[32] = {0};
bool hasCurrentContact = false;
bool night_mode_active = false;
char myNodeName[32] = {0};
lv_obj_t* ui_ContactStatus = nullptr;
lv_obj_t* ui_HomeNodeName = nullptr;
lv_obj_t* ui_HomePubKey = nullptr;
lv_obj_t* ui_HomeInfo = nullptr;
lv_obj_t* ui_AdvertiseBtn = nullptr;
lv_obj_t* ui_SettingsName = nullptr;
lv_obj_t* ui_SettingsPreset = nullptr;
lv_obj_t* ui_SettingsFreq = nullptr;
lv_obj_t* ui_SettingsBw = nullptr;
lv_obj_t* ui_SettingsSf = nullptr;
lv_obj_t* ui_SettingsCr = nullptr;
lv_obj_t* ui_SettingsTx = nullptr;
lv_obj_t* ui_SettingsFw = nullptr;
lv_obj_t* ui_SettingsSaveBtn = nullptr;
lv_obj_t* ui_SettingsStatus = nullptr;
// Whichever input/send button last got focus — used by keyboard show/hide
lv_obj_t* activeInput = nullptr;
lv_obj_t* activeSendBtn = nullptr;
int activeInputBaseY = 0;
public:
UIManager();
void onChannelInputFocus(lv_event_t* e);
void onContactInputFocus(lv_event_t* e);
void onContactSendClick(lv_event_t* e);
void onSettingsInputFocus(lv_event_t* e);
void onSettingsSaveClick(lv_event_t* e);
void onAdvertiseClick(lv_event_t* e);
void onDimOverlayClick(lv_event_t* e);
void onSendClick(lv_event_t* e);
void onKeyboardEvent(lv_event_t* e);
void scroll_begin_event(lv_event_t* e);
void onPresetChange(uint16_t idx);
void populateSettings(const char* name, float freq, float bw, uint8_t sf, uint8_t cr,
uint8_t tx_power, const char* fw_ver, const char* build_date);
void populateHome(const char* name, const char* pub_key_hex,
int contact_count, float freq);
void setMyNodeName(const char* name);
// Phase 6
void routeIncomingDM(const uint8_t* from_pub_key, const char* from_name,
const char* time_str, const char* text);
void setSendStatus(int state); // 0=sending, 1=delivered, 2=failed, -1=clear
void updateDateTime(const struct tm timeinfo);
void updateInfo(const char *str, uint32_t color);
void clearDateTime();
void updateValues();
void addPrivateChatBubble(const char *time_str, const char *msg, bool is_self, bool do_scroll = true);
void addChatBubble(const char *time_str, const char *sender, const char *msg, bool is_self, bool do_scroll = true);
void scrollPrivateChatToBottom();
void scrollPublicChatToBottom();
void beginPublicHistoryLoad(); // disable flex before bulk load
void endPublicHistoryLoad(); // re-enable flex after bulk load
void addContactToUI(ContactInfo c);
void updateContactLastSeen(const uint8_t* pub_key, uint32_t lastmod);
void refreshLastSeenLabels();
void handleContactClick(lv_event_t *e);
void setNightMode(bool night);
};
#endif

12
include/uiTasks.h

@ -0,0 +1,12 @@
#ifndef TASKS_h
#define TASKS_h
#include <Arduino.h>
#include "uiDefines.h"
#include "uiVars.h"
void lvgl_task(void *pvParameters);
void clock_task(void *pvParameters);
void core_task(void *pvParameters);
#endif

220
include/uiTouch.h

@ -0,0 +1,220 @@
/*******************************************************************************
* Touch libraries:
* FT6X36: https://github.com/strange-v/FT6X36.git
* GT911: https://github.com/TAMCTec/gt911-arduino.git
* XPT2046: https://github.com/PaulStoffregen/XPT2046_Touchscreen.git
******************************************************************************/
/* uncomment for FT6X36 */
// #define TOUCH_FT6X36
// #define TOUCH_FT6X36_SCL 38//19
// #define TOUCH_FT6X36_SDA 37//18
// #define TOUCH_FT6X36_INT 4//39
// #define TOUCH_SWAP_XY
// #define TOUCH_MAP_X1 800
// #define TOUCH_MAP_X2 0
// #define TOUCH_MAP_Y1 0
// #define TOUCH_MAP_Y2 480
/* uncomment for GT911 */
// #define TOUCH_GT911
// #define TOUCH_GT911_SCL 20//20
// #define TOUCH_GT911_SDA 19//19
// #define TOUCH_GT911_INT -1//-1
// #define TOUCH_GT911_RST -1//38
// #define TOUCH_GT911_ROTATION ROTATION_NORMAL
// #define TOUCH_MAP_X1 800//480
// #define TOUCH_MAP_X2 0
// #define TOUCH_MAP_Y1 480//272
// #define TOUCH_MAP_Y2 0
/* uncomment for XPT2046 */
// #define TOUCH_XPT2046
// #define TOUCH_XPT2046_SCK 12
// #define TOUCH_XPT2046_MISO 13
// #define TOUCH_XPT2046_MOSI 11
// #define TOUCH_XPT2046_CS 38
// #define TOUCH_XPT2046_INT 36
// #define TOUCH_XPT2046_ROTATION 0
// #define TOUCH_MAP_X1 4000
// #define TOUCH_MAP_X2 100
// #define TOUCH_MAP_Y1 100
// #define TOUCH_MAP_Y2 4000
LGFX lcd;
int touch_last_x = 0, touch_last_y = 0;
#if defined(TOUCH_FT6X36)
#include <Wire.h>
#include <FT6X36.h>
FT6X36 ts(&Wire, TOUCH_FT6X36_INT);
bool touch_touched_flag = true, touch_released_flag = true;
#elif defined(TOUCH_GT911)
#include <Wire.h>
#include <TAMC_GT911.h>
TAMC_GT911 ts = TAMC_GT911(TOUCH_GT911_SDA, TOUCH_GT911_SCL, TOUCH_GT911_INT, TOUCH_GT911_RST, max(TOUCH_MAP_X1, TOUCH_MAP_X2), max(TOUCH_MAP_Y1, TOUCH_MAP_Y2));
#elif defined(TOUCH_XPT2046)
#include <XPT2046_Touchscreen.h>
#include <SPI.h>
XPT2046_Touchscreen ts(TOUCH_XPT2046_CS, TOUCH_XPT2046_INT);
#endif
#if defined(TOUCH_FT6X36)
void touch(TPoint p, TEvent e)
{
if (e != TEvent::Tap && e != TEvent::DragStart && e != TEvent::DragMove && e != TEvent::DragEnd)
{
return;
}
// translation logic depends on screen rotation
#if defined(TOUCH_SWAP_XY)
touch_last_x = map(p.y, TOUCH_MAP_X1, TOUCH_MAP_X2, 0, lcd.width());
touch_last_y = map(p.x, TOUCH_MAP_Y1, TOUCH_MAP_Y2, 0, lcd.height());
#else
touch_last_x = map(p.x, TOUCH_MAP_X1, TOUCH_MAP_X2, 0, lcd.width());
touch_last_y = map(p.y, TOUCH_MAP_Y1, TOUCH_MAP_Y2, 0, lcd.height());
#endif
switch (e)
{
case TEvent::Tap:
Serial.println("Tap");
touch_touched_flag = true;
touch_released_flag = true;
break;
case TEvent::DragStart:
Serial.println("DragStart");
touch_touched_flag = true;
break;
case TEvent::DragMove:
Serial.println("DragMove");
touch_touched_flag = true;
break;
case TEvent::DragEnd:
Serial.println("DragEnd");
touch_released_flag = true;
break;
default:
Serial.println("UNKNOWN");
break;
}
}
#endif
void touch_init()
{
#if defined(TOUCH_FT6X36)
Wire.begin(TOUCH_FT6X36_SDA, TOUCH_FT6X36_SCL);
ts.begin();
ts.registerTouchHandler(touch);
#elif defined(TOUCH_GT911)
Wire.begin(TOUCH_GT911_SDA, TOUCH_GT911_SCL);
ts.begin();
ts.setRotation(TOUCH_GT911_ROTATION);
#elif defined(TOUCH_XPT2046)
SPI.begin(TOUCH_XPT2046_SCK, TOUCH_XPT2046_MISO, TOUCH_XPT2046_MOSI, TOUCH_XPT2046_CS);
ts.begin();
ts.setRotation(TOUCH_XPT2046_ROTATION);
#endif
}
bool touch_has_signal()
{
#if defined(TOUCH_FT6X36)
ts.loop();
return touch_touched_flag || touch_released_flag;
#elif defined(TOUCH_GT911)
return true;
#elif defined(TOUCH_XPT2046)
return ts.tirqTouched();
#else
return false;
#endif
}
bool touch_touched()
{
#if defined(TOUCH_FT6X36)
if (touch_touched_flag)
{
touch_touched_flag = false;
return true;
}
else
{
return false;
}
#elif defined(TOUCH_GT911)
ts.read();
if (ts.isTouched)
{
#if defined(TOUCH_SWAP_XY)
touch_last_x = map(ts.points[0].y, TOUCH_MAP_X1, TOUCH_MAP_X2, 0, lcd.width() - 1);
touch_last_y = map(ts.points[0].x, TOUCH_MAP_Y1, TOUCH_MAP_Y2, 0, lcd.height() - 1);
#else
touch_last_x = map(ts.points[0].x, TOUCH_MAP_X1, TOUCH_MAP_X2, 0, lcd.width() - 1);
touch_last_y = map(ts.points[0].y, TOUCH_MAP_Y1, TOUCH_MAP_Y2, 0, lcd.height() - 1);
#endif
return true;
}
else
{
return false;
}
#elif defined(TOUCH_XPT2046)
if (ts.touched())
{
TS_Point p = ts.getPoint();
#if defined(TOUCH_SWAP_XY)
touch_last_x = map(p.y, TOUCH_MAP_X1, TOUCH_MAP_X2, 0, lcd.width() - 1);
touch_last_y = map(p.x, TOUCH_MAP_Y1, TOUCH_MAP_Y2, 0, lcd.height() - 1);
#else
touch_last_x = map(p.x, TOUCH_MAP_X1, TOUCH_MAP_X2, 0, lcd.width() - 1);
touch_last_y = map(p.y, TOUCH_MAP_Y1, TOUCH_MAP_Y2, 0, lcd.height() - 1);
#endif
return true;
}
else
{
return false;
}
#else
return false;
#endif
}
bool touch_released()
{
#if defined(TOUCH_FT6X36)
if (touch_released_flag)
{
touch_released_flag = false;
return true;
}
else
{
return false;
}
#elif defined(TOUCH_GT911)
return true;
#elif defined(TOUCH_XPT2046)
return true;
#else
return false;
#endif
}

22
include/uiVars.h

@ -0,0 +1,22 @@
#ifndef VARS_h
#define VARS_h
#include <Arduino.h>
#include "lvgl.h"
#include "uiManager.h"
extern void createTasks();
extern TaskHandle_t t_core1_tft;
extern TaskHandle_t t_core0_lvgl;
extern TaskHandle_t t_core1_clock;
extern TaskHandle_t t_core1_core;
extern SemaphoreHandle_t semaphoreData;
extern UIManager *uiManager;
extern lv_obj_t * ui_MainTabView;
extern volatile bool g_clock_synced;
#endif

787
lib/lvgl/lv_conf.h

@ -0,0 +1,787 @@
/**
* @file lv_conf.h
* Configuration file for v8.3.11
*/
/*
* Copy this file as `lv_conf.h`
* 1. simply next to the `lvgl` folder
* 2. or any other places and
* - define `LV_CONF_INCLUDE_SIMPLE`
* - add the path as include path
*/
/* clang-format off */
#if 1 /*Set it to "1" to enable content*/
#ifndef LV_CONF_H
#define LV_CONF_H
#include <stdint.h>
/*====================
COLOR SETTINGS
*====================*/
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 0
/*Enable features to draw on transparent background.
*It's required if opa, and transform_* style properties are used.
*Can be also used if the UI is above another layer, e.g. an OSD menu or video player.*/
#define LV_COLOR_SCREEN_TRANSP 0
/* Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently.
* 0: round down, 64: round up from x.75, 128: round up from half, 192: round up from x.25, 254: round up */
#define LV_COLOR_MIX_ROUND_OFS 0
/*Images pixels with this color will not be drawn if they are chroma keyed)*/
#define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) /*pure green*/
/*=========================
MEMORY SETTINGS
*=========================*/
/*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/
/* ESP32-S3 has 8 MB OPI PSRAM. Switching to LV_MEM_CUSTOM=1 lets LVGL use the
system heap (which includes PSRAM on this board) instead of a fixed 48 KB
static pool. This prevents OOM crashes when loading many chat bubbles. */
#define LV_MEM_CUSTOM 1
#if LV_MEM_CUSTOM == 0
/*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (48U * 1024U) /*[bytes]*/
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
#define LV_MEM_ADR 0 /*0: unused*/
/*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
#if LV_MEM_ADR == 0
#undef LV_MEM_POOL_INCLUDE
#undef LV_MEM_POOL_ALLOC
#endif
#else /*LV_MEM_CUSTOM*/
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
#endif /*LV_MEM_CUSTOM*/
/*Number of the intermediate memory buffer used during rendering and other internal processing mechanisms.
*You will see an error log message if there wasn't enough buffers. */
#define LV_MEM_BUF_MAX_NUM 16
/*Use the standard `memcpy` and `memset` instead of LVGL's own functions. (Might or might not be faster).*/
#define LV_MEMCPY_MEMSET_STD 0
/*====================
HAL SETTINGS
*====================*/
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/
/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 30 /*[ms]*/
/*Use a custom tick source that tells the elapsed time in milliseconds.
*It removes the need to manually update the tick with `lv_tick_inc()`)*/
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
#define LV_TICK_CUSTOM_INCLUDE "Arduino.h" /*Header for the system time function*/
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis()) /*Expression evaluating to current system time in ms*/
/*If using lvgl as ESP32 component*/
// #define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
// #define LV_TICK_CUSTOM_SYS_TIME_EXPR ((esp_timer_get_time() / 1000LL))
#endif /*LV_TICK_CUSTOM*/
/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings.
*(Not so important, you can adjust it to modify default sizes and spaces)*/
#define LV_DPI_DEF 130 /*[px/inch]*/
/*=======================
* FEATURE CONFIGURATION
*=======================*/
/*-------------
* Drawing
*-----------*/
/*Enable complex draw engine.
*Required to draw shadow, gradient, rounded corners, circles, arc, skew lines, image transformations or any masks*/
#define LV_DRAW_COMPLEX 1
#if LV_DRAW_COMPLEX != 0
/*Allow buffering some shadow calculation.
*LV_SHADOW_CACHE_SIZE is the max. shadow size to buffer, where shadow size is `shadow_width + radius`
*Caching has LV_SHADOW_CACHE_SIZE^2 RAM cost*/
#define LV_SHADOW_CACHE_SIZE 0
/* Set number of maximally cached circle data.
* The circumference of 1/4 circle are saved for anti-aliasing
* radius * 4 bytes are used per circle (the most often used radiuses are saved)
* 0: to disable caching */
#define LV_CIRCLE_CACHE_SIZE 4
#endif /*LV_DRAW_COMPLEX*/
/**
* "Simple layers" are used when a widget has `style_opa < 255` to buffer the widget into a layer
* and blend it as an image with the given opacity.
* Note that `bg_opa`, `text_opa` etc don't require buffering into layer)
* The widget can be buffered in smaller chunks to avoid using large buffers.
*
* - LV_LAYER_SIMPLE_BUF_SIZE: [bytes] the optimal target buffer size. LVGL will try to allocate it
* - LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE: [bytes] used if `LV_LAYER_SIMPLE_BUF_SIZE` couldn't be allocated.
*
* Both buffer sizes are in bytes.
* "Transformed layers" (where transform_angle/zoom properties are used) use larger buffers
* and can't be drawn in chunks. So these settings affects only widgets with opacity.
*/
#define LV_LAYER_SIMPLE_BUF_SIZE (24 * 1024)
#define LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE (3 * 1024)
/*Default image cache size. Image caching keeps the images opened.
*If only the built-in image formats are used there is no real advantage of caching. (I.e. if no new image decoder is added)
*With complex image decoders (e.g. PNG or JPG) caching can save the continuous open/decode of images.
*However the opened images might consume additional RAM.
*0: to disable caching*/
#define LV_IMG_CACHE_DEF_SIZE 0
/*Number of stops allowed per gradient. Increase this to allow more stops.
*This adds (sizeof(lv_color_t) + 1) bytes per additional stop*/
#define LV_GRADIENT_MAX_STOPS 2
/*Default gradient buffer size.
*When LVGL calculates the gradient "maps" it can save them into a cache to avoid calculating them again.
*LV_GRAD_CACHE_DEF_SIZE sets the size of this cache in bytes.
*If the cache is too small the map will be allocated only while it's required for the drawing.
*0 mean no caching.*/
#define LV_GRAD_CACHE_DEF_SIZE 0
/*Allow dithering the gradients (to achieve visual smooth color gradients on limited color depth display)
*LV_DITHER_GRADIENT implies allocating one or two more lines of the object's rendering surface
*The increase in memory consumption is (32 bits * object width) plus 24 bits * object width if using error diffusion */
#define LV_DITHER_GRADIENT 0
#if LV_DITHER_GRADIENT
/*Add support for error diffusion dithering.
*Error diffusion dithering gets a much better visual result, but implies more CPU consumption and memory when drawing.
*The increase in memory consumption is (24 bits * object's width)*/
#define LV_DITHER_ERROR_DIFFUSION 0
#endif
/*Maximum buffer size to allocate for rotation.
*Only used if software rotation is enabled in the display driver.*/
#define LV_DISP_ROT_MAX_BUF (10*1024)
/*-------------
* GPU
*-----------*/
/*Use Arm's 2D acceleration library Arm-2D */
#define LV_USE_GPU_ARM2D 0
/*Use STM32's DMA2D (aka Chrom Art) GPU*/
#define LV_USE_GPU_STM32_DMA2D 0
#if LV_USE_GPU_STM32_DMA2D
/*Must be defined to include path of CMSIS header of target processor
e.g. "stm32f7xx.h" or "stm32f4xx.h"*/
#define LV_GPU_DMA2D_CMSIS_INCLUDE
#endif
/*Enable RA6M3 G2D GPU*/
#define LV_USE_GPU_RA6M3_G2D 0
#if LV_USE_GPU_RA6M3_G2D
/*include path of target processor
e.g. "hal_data.h"*/
#define LV_GPU_RA6M3_G2D_INCLUDE "hal_data.h"
#endif
/*Use SWM341's DMA2D GPU*/
#define LV_USE_GPU_SWM341_DMA2D 0
#if LV_USE_GPU_SWM341_DMA2D
#define LV_GPU_SWM341_DMA2D_INCLUDE "SWM341.h"
#endif
/*Use NXP's PXP GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_PXP 0
#if LV_USE_GPU_NXP_PXP
/*1: Add default bare metal and FreeRTOS interrupt handling routines for PXP (lv_gpu_nxp_pxp_osa.c)
* and call lv_gpu_nxp_pxp_init() automatically during lv_init(). Note that symbol SDK_OS_FREE_RTOS
* has to be defined in order to use FreeRTOS OSA, otherwise bare-metal implementation is selected.
*0: lv_gpu_nxp_pxp_init() has to be called manually before lv_init()
*/
#define LV_USE_GPU_NXP_PXP_AUTO_INIT 0
#endif
/*Use NXP's VG-Lite GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_VG_LITE 0
/*Use SDL renderer API*/
#define LV_USE_GPU_SDL 0
#if LV_USE_GPU_SDL
#define LV_GPU_SDL_INCLUDE_PATH <SDL2/SDL.h>
/*Texture cache size, 8MB by default*/
#define LV_GPU_SDL_LRU_SIZE (1024 * 1024 * 8)
/*Custom blend mode for mask drawing, disable if you need to link with older SDL2 lib*/
#define LV_GPU_SDL_CUSTOM_BLEND_MODE (SDL_VERSION_ATLEAST(2, 0, 6))
#endif
/*-------------
* Logging
*-----------*/
/*Enable the log module*/
#define LV_USE_LOG 0
#if LV_USE_LOG
/*How important log should be added:
*LV_LOG_LEVEL_TRACE A lot of logs to give detailed information
*LV_LOG_LEVEL_INFO Log important events
*LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem
*LV_LOG_LEVEL_ERROR Only critical issue, when the system may fail
*LV_LOG_LEVEL_USER Only logs added by the user
*LV_LOG_LEVEL_NONE Do not log anything*/
#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN
/*1: Print the log with 'printf';
*0: User need to register a callback with `lv_log_register_print_cb()`*/
#define LV_LOG_PRINTF 0
/*Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs*/
#define LV_LOG_TRACE_MEM 1
#define LV_LOG_TRACE_TIMER 1
#define LV_LOG_TRACE_INDEV 1
#define LV_LOG_TRACE_DISP_REFR 1
#define LV_LOG_TRACE_EVENT 1
#define LV_LOG_TRACE_OBJ_CREATE 1
#define LV_LOG_TRACE_LAYOUT 1
#define LV_LOG_TRACE_ANIM 1
#endif /*LV_USE_LOG*/
/*-------------
* Asserts
*-----------*/
/*Enable asserts if an operation is failed or an invalid data is found.
*If LV_USE_LOG is enabled an error message will be printed on failure*/
#define LV_USE_ASSERT_NULL 1 /*Check if the parameter is NULL. (Very fast, recommended)*/
#define LV_USE_ASSERT_MALLOC 1 /*Checks is the memory is successfully allocated or no. (Very fast, recommended)*/
#define LV_USE_ASSERT_STYLE 0 /*Check if the styles are properly initialized. (Very fast, recommended)*/
#define LV_USE_ASSERT_MEM_INTEGRITY 0 /*Check the integrity of `lv_mem` after critical operations. (Slow)*/
#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/
/*Add a custom handler when assert happens e.g. to restart the MCU*/
#define LV_ASSERT_HANDLER_INCLUDE <stdint.h>
#define LV_ASSERT_HANDLER while(1); /*Halt by default*/
/*-------------
* Others
*-----------*/
/*1: Show CPU usage and FPS count*/
#define LV_USE_PERF_MONITOR 0
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
/*1: Show the used memory and the memory fragmentation
* Requires LV_MEM_CUSTOM = 0*/
#define LV_USE_MEM_MONITOR 0
#if LV_USE_MEM_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif
/*1: Draw random colored rectangles over the redrawn areas*/
#define LV_USE_REFR_DEBUG 0
/*Change the built in (v)snprintf functions*/
#define LV_SPRINTF_CUSTOM 0
#if LV_SPRINTF_CUSTOM
#define LV_SPRINTF_INCLUDE <stdio.h>
#define lv_snprintf snprintf
#define lv_vsnprintf vsnprintf
#else /*LV_SPRINTF_CUSTOM*/
#define LV_SPRINTF_USE_FLOAT 0
#endif /*LV_SPRINTF_CUSTOM*/
#define LV_USE_USER_DATA 1
/*Garbage Collector settings
*Used if lvgl is bound to higher level language and the memory is managed by that language*/
#define LV_ENABLE_GC 0
#if LV_ENABLE_GC != 0
#define LV_GC_INCLUDE "gc.h" /*Include Garbage Collector related things*/
#endif /*LV_ENABLE_GC*/
/*=====================
* COMPILER SETTINGS
*====================*/
/*For big endian systems set to 1*/
#define LV_BIG_ENDIAN_SYSTEM 0
/*Define a custom attribute to `lv_tick_inc` function*/
#define LV_ATTRIBUTE_TICK_INC
/*Define a custom attribute to `lv_timer_handler` function*/
#define LV_ATTRIBUTE_TIMER_HANDLER
/*Define a custom attribute to `lv_disp_flush_ready` function*/
#define LV_ATTRIBUTE_FLUSH_READY
/*Required alignment size for buffers*/
#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1
/*Will be added where memories needs to be aligned (with -Os data might not be aligned to boundary by default).
* E.g. __attribute__((aligned(4)))*/
#define LV_ATTRIBUTE_MEM_ALIGN
/*Attribute to mark large constant arrays for example font's bitmaps*/
#define LV_ATTRIBUTE_LARGE_CONST
/*Compiler prefix for a big array declaration in RAM*/
#define LV_ATTRIBUTE_LARGE_RAM_ARRAY
/*Place performance critical functions into a faster memory (e.g RAM)*/
#define LV_ATTRIBUTE_FAST_MEM
/*Prefix variables that are used in GPU accelerated operations, often these need to be placed in RAM sections that are DMA accessible*/
#define LV_ATTRIBUTE_DMA
/*Export integer constant to binding. This macro is used with constants in the form of LV_<CONST> that
*should also appear on LVGL binding API such as Micropython.*/
#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /*The default value just prevents GCC warning*/
/*Extend the default -32k..32k coordinate range to -4M..4M by using int32_t for coordinates instead of int16_t*/
#define LV_USE_LARGE_COORD 0
/*==================
* FONT USAGE
*===================*/
/*Montserrat fonts with ASCII range and some symbols using bpp = 4
*https://fonts.google.com/specimen/Montserrat*/
#define LV_FONT_MONTSERRAT_8 0
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_MONTSERRAT_18 1
#define LV_FONT_MONTSERRAT_20 1
#define LV_FONT_MONTSERRAT_22 1
#define LV_FONT_MONTSERRAT_24 1
#define LV_FONT_MONTSERRAT_26 1
#define LV_FONT_MONTSERRAT_28 0
#define LV_FONT_MONTSERRAT_30 0
#define LV_FONT_MONTSERRAT_32 1
#define LV_FONT_MONTSERRAT_34 0
#define LV_FONT_MONTSERRAT_36 0
#define LV_FONT_MONTSERRAT_38 0
#define LV_FONT_MONTSERRAT_40 1
#define LV_FONT_MONTSERRAT_42 0
#define LV_FONT_MONTSERRAT_44 1
#define LV_FONT_MONTSERRAT_46 0
#define LV_FONT_MONTSERRAT_48 1
/*Demonstrate special features*/
#define LV_FONT_MONTSERRAT_12_SUBPX 0
#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
/*Pixel perfect monospace fonts*/
#define LV_FONT_UNSCII_8 0
#define LV_FONT_UNSCII_16 0
/*Optionally declare custom fonts here.
*You can use these fonts as default font too and they will be available globally.
*E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/
#define LV_FONT_CUSTOM_DECLARE
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_14
/*Enable handling large font and/or fonts with a lot of characters.
*The limit depends on the font size, font face and bpp.
*Compiler error will be triggered if a font needs it.*/
#define LV_FONT_FMT_TXT_LARGE 0
/*Enables/disables support for compressed fonts.*/
#define LV_USE_FONT_COMPRESSED 0
/*Enable subpixel rendering*/
#define LV_USE_FONT_SUBPX 0
#if LV_USE_FONT_SUBPX
/*Set the pixel order of the display. Physical order of RGB channels. Doesn't matter with "normal" fonts.*/
#define LV_FONT_SUBPX_BGR 0 /*0: RGB; 1:BGR order*/
#endif
/*Enable drawing placeholders when glyph dsc is not found*/
#define LV_USE_FONT_PLACEHOLDER 1
/*=================
* TEXT SETTINGS
*=================*/
/**
* Select a character encoding for strings.
* Your IDE or editor should have the same character encoding
* - LV_TXT_ENC_UTF8
* - LV_TXT_ENC_ASCII
*/
#define LV_TXT_ENC LV_TXT_ENC_UTF8
/*Can break (wrap) texts on these chars*/
#define LV_TXT_BREAK_CHARS " ,.;:-_"
/*If a word is at least this long, will break wherever "prettiest"
*To disable, set to a value <= 0*/
#define LV_TXT_LINE_BREAK_LONG_LEN 0
/*Minimum number of characters in a long word to put on a line before a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3
/*Minimum number of characters in a long word to put on a line after a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3
/*The control character to use for signalling text recoloring.*/
#define LV_TXT_COLOR_CMD "#"
/*Support bidirectional texts. Allows mixing Left-to-Right and Right-to-Left texts.
*The direction will be processed according to the Unicode Bidirectional Algorithm:
*https://www.w3.org/International/articles/inline-bidi-markup/uba-basics*/
#define LV_USE_BIDI 0
#if LV_USE_BIDI
/*Set the default direction. Supported values:
*`LV_BASE_DIR_LTR` Left-to-Right
*`LV_BASE_DIR_RTL` Right-to-Left
*`LV_BASE_DIR_AUTO` detect texts base direction*/
#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO
#endif
/*Enable Arabic/Persian processing
*In these languages characters should be replaced with an other form based on their position in the text*/
#define LV_USE_ARABIC_PERSIAN_CHARS 0
/*==================
* WIDGET USAGE
*================*/
/*Documentation of the widgets: https://docs.lvgl.io/latest/en/html/widgets/index.html*/
#define LV_USE_ARC 0
#define LV_USE_BAR 0
#define LV_USE_BTN 1
#define LV_USE_BTNMATRIX 1
#define LV_USE_CANVAS 1
#define LV_USE_CHECKBOX 1
#define LV_USE_DROPDOWN 1 /*Requires: lv_label*/
#define LV_USE_IMG 1 /*Requires: lv_label*/
#define LV_USE_LABEL 1
#if LV_USE_LABEL
#define LV_LABEL_TEXT_SELECTION 1 /*Enable selecting text of the label*/
#define LV_LABEL_LONG_TXT_HINT 1 /*Store some extra info in labels to speed up drawing of very long texts*/
#endif
#define LV_USE_LINE 0
#define LV_USE_ROLLER 1 /*Requires: lv_label*/
#if LV_USE_ROLLER
#define LV_ROLLER_INF_PAGES 7 /*Number of extra "pages" when the roller is infinite*/
#endif
#define LV_USE_SLIDER 0 /*Requires: lv_bar*/
#define LV_USE_SWITCH 0
#define LV_USE_TEXTAREA 1 /*Requires: lv_label*/
#if LV_USE_TEXTAREA != 0
#define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/
#endif
#define LV_USE_TABLE 0
/*==================
* EXTRA COMPONENTS
*==================*/
/*-----------
* Widgets
*----------*/
#define LV_USE_ANIMIMG 0
#define LV_USE_CALENDAR 0
#if LV_USE_CALENDAR
#define LV_CALENDAR_WEEK_STARTS_MONDAY 0
#if LV_CALENDAR_WEEK_STARTS_MONDAY
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
#else
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
#endif
#define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
#define LV_USE_CALENDAR_HEADER_ARROW 1
#define LV_USE_CALENDAR_HEADER_DROPDOWN 1
#endif /*LV_USE_CALENDAR*/
#define LV_USE_CHART 0
#define LV_USE_COLORWHEEL 0
#define LV_USE_IMGBTN 1
#define LV_USE_KEYBOARD 1
#define LV_USE_LED 0
#define LV_USE_LIST 1
#define LV_USE_MENU 1
#define LV_USE_METER 0
#define LV_USE_MSGBOX 1
#define LV_USE_SPAN 1
#if LV_USE_SPAN
/*A line text can contain maximum num of span descriptor */
#define LV_SPAN_SNIPPET_STACK_SIZE 64
#endif
#define LV_USE_SPINBOX 0
#define LV_USE_SPINNER 0
#define LV_USE_TABVIEW 1
#define LV_USE_TILEVIEW 0
#define LV_USE_WIN 0
/*-----------
* Themes
*----------*/
/*A simple, impressive and very complete theme*/
#define LV_USE_THEME_DEFAULT 1
#if LV_USE_THEME_DEFAULT
/*0: Light mode; 1: Dark mode*/
#define LV_THEME_DEFAULT_DARK 0
/*1: Enable grow on press*/
#define LV_THEME_DEFAULT_GROW 1
/*Default transition time in [ms]*/
#define LV_THEME_DEFAULT_TRANSITION_TIME 80
#endif /*LV_USE_THEME_DEFAULT*/
/*A very simple theme that is a good starting point for a custom theme*/
#define LV_USE_THEME_BASIC 1
/*A theme designed for monochrome displays*/
#define LV_USE_THEME_MONO 1
/*-----------
* Layouts
*----------*/
/*A layout similar to Flexbox in CSS.*/
#define LV_USE_FLEX 1
/*A layout similar to Grid in CSS.*/
#define LV_USE_GRID 1
/*---------------------
* 3rd party libraries
*--------------------*/
/*File system interfaces for common APIs */
/*API for fopen, fread, etc*/
#define LV_USE_FS_STDIO 0
#if LV_USE_FS_STDIO
#define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for open, read, etc*/
#define LV_USE_FS_POSIX 0
#if LV_USE_FS_POSIX
#define LV_FS_POSIX_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_POSIX_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_POSIX_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for CreateFile, ReadFile, etc*/
#define LV_USE_FS_WIN32 0
#if LV_USE_FS_WIN32
#define LV_FS_WIN32_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_WIN32_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_WIN32_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/
#define LV_USE_FS_FATFS 0
#if LV_USE_FS_FATFS
#define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for LittleFS (library needs to be added separately). Uses lfs_file_open, lfs_file_read, etc*/
#define LV_USE_FS_LITTLEFS 0
#if LV_USE_FS_LITTLEFS
#define LV_FS_LITTLEFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_LITTLEFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*PNG decoder library*/
#define LV_USE_PNG 0
/*BMP decoder library*/
#define LV_USE_BMP 0
/* JPG + split JPG decoder library.
* Split JPG is a custom format optimized for embedded systems. */
#define LV_USE_SJPG 0
/*GIF decoder library*/
#define LV_USE_GIF 0
/*QR code library*/
#define LV_USE_QRCODE 0
/*FreeType library*/
#define LV_USE_FREETYPE 0
#if LV_USE_FREETYPE
/*Memory used by FreeType to cache characters [bytes] (-1: no caching)*/
#define LV_FREETYPE_CACHE_SIZE (16 * 1024)
#if LV_FREETYPE_CACHE_SIZE >= 0
/* 1: bitmap cache use the sbit cache, 0:bitmap cache use the image cache. */
/* sbit cache:it is much more memory efficient for small bitmaps(font size < 256) */
/* if font size >= 256, must be configured as image cache */
#define LV_FREETYPE_SBIT_CACHE 0
/* Maximum number of opened FT_Face/FT_Size objects managed by this cache instance. */
/* (0:use system defaults) */
#define LV_FREETYPE_CACHE_FT_FACES 0
#define LV_FREETYPE_CACHE_FT_SIZES 0
#endif
#endif
/*Tiny TTF library*/
#define LV_USE_TINY_TTF 0
#if LV_USE_TINY_TTF
/*Load TTF data from files*/
#define LV_TINY_TTF_FILE_SUPPORT 0
#endif
/*Rlottie library*/
#define LV_USE_RLOTTIE 0
/*FFmpeg library for image decoding and playing videos
*Supports all major image formats so do not enable other image decoder with it*/
#define LV_USE_FFMPEG 0
#if LV_USE_FFMPEG
/*Dump input information to stderr*/
#define LV_FFMPEG_DUMP_FORMAT 0
#endif
/*-----------
* Others
*----------*/
/*1: Enable API to take snapshot for object*/
#define LV_USE_SNAPSHOT 0
/*1: Enable Monkey test*/
#define LV_USE_MONKEY 0
/*1: Enable grid navigation*/
#define LV_USE_GRIDNAV 0
/*1: Enable lv_obj fragment*/
#define LV_USE_FRAGMENT 0
/*1: Support using images as font in label or span widgets */
#define LV_USE_IMGFONT 0
/*1: Enable a published subscriber based messaging system */
#define LV_USE_MSG 0
/*1: Enable Pinyin input method*/
/*Requires: lv_keyboard*/
#define LV_USE_IME_PINYIN 0
#if LV_USE_IME_PINYIN
/*1: Use default thesaurus*/
/*If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesauruss*/
#define LV_IME_PINYIN_USE_DEFAULT_DICT 1
/*Set the maximum number of candidate panels that can be displayed*/
/*This needs to be adjusted according to the size of the screen*/
#define LV_IME_PINYIN_CAND_TEXT_NUM 6
/*Use 9 key input(k9)*/
#define LV_IME_PINYIN_USE_K9_MODE 1
#if LV_IME_PINYIN_USE_K9_MODE == 1
#define LV_IME_PINYIN_K9_CAND_TEXT_NUM 3
#endif // LV_IME_PINYIN_USE_K9_MODE
#endif
/*==================
* EXAMPLES
*==================*/
/*Enable the examples to be built with the library*/
#define LV_BUILD_EXAMPLES 0
/*===================
* DEMO USAGE
====================*/
/*Show some widget. It might be required to increase `LV_MEM_SIZE` */
#define LV_USE_DEMO_WIDGETS 0
#if LV_USE_DEMO_WIDGETS
#define LV_DEMO_WIDGETS_SLIDESHOW 0
#endif
/*Demonstrate the usage of encoder and keyboard*/
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0
/*Benchmark your system*/
#define LV_USE_DEMO_BENCHMARK 0
#if LV_USE_DEMO_BENCHMARK
/*Use RGB565A8 images with 16 bit color depth instead of ARGB8565*/
#define LV_DEMO_BENCHMARK_RGB565A8 0
#endif
/*Stress test for LVGL*/
#define LV_USE_DEMO_STRESS 0
/*Music player demo*/
#define LV_USE_DEMO_MUSIC 0
#if LV_USE_DEMO_MUSIC
#define LV_DEMO_MUSIC_SQUARE 0
#define LV_DEMO_MUSIC_LANDSCAPE 0
#define LV_DEMO_MUSIC_ROUND 0
#define LV_DEMO_MUSIC_LARGE 0
#define LV_DEMO_MUSIC_AUTO_PLAY 0
#endif
/*--END OF LV_CONF_H--*/
#endif /*LV_CONF_H*/
#endif /*End of "Content enable"*/

7
partition-table-8MB.csv

@ -0,0 +1,7 @@
# This is a layout for 8MB of flash for MUI devices
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x5C0000,
flashApp, app, ota_1, 0x5D0000,0x0A0000,
spiffs, data, spiffs, 0x670000,0x180000
Can't render this file because it has a wrong number of fields in line 2.

25
src/fonts/fonts.h

@ -0,0 +1,25 @@
extern const lv_font_t lv_font_arial_8;
extern const lv_font_t lv_font_arial_10;
extern const lv_font_t lv_font_arial_12;
extern const lv_font_t lv_font_arial_14;
extern const lv_font_t lv_font_arial_16;
extern const lv_font_t lv_font_arial_18;
extern const lv_font_t lv_font_arial_20;
extern const lv_font_t lv_font_arial_22;
extern const lv_font_t lv_font_arial_24;
extern const lv_font_t lv_font_arial_26;
extern const lv_font_t lv_font_arial_28;
extern const lv_font_t lv_font_arial_30;
extern const lv_font_t lv_font_arial_32;
extern const lv_font_t lv_font_arial_34;
extern const lv_font_t lv_font_arial_36;
extern const lv_font_t lv_font_arial_38;
extern const lv_font_t lv_font_arial_40;
extern const lv_font_t lv_font_arial_42;
extern const lv_font_t lv_font_arial_44;
extern const lv_font_t lv_font_arial_46;
extern const lv_font_t lv_font_arial_48;
extern const lv_font_t lv_font_arial_50;
extern const lv_font_t lv_font_arial_58;
extern const lv_font_t lv_font_arial_66;
extern const lv_font_t lv_font_arial_74;

1244
src/fonts/lv_font_arial_10.c

File diff suppressed because it is too large

1358
src/fonts/lv_font_arial_12.c

File diff suppressed because it is too large

1687
src/fonts/lv_font_arial_14.c

File diff suppressed because it is too large

1881
src/fonts/lv_font_arial_16.c

File diff suppressed because it is too large

2201
src/fonts/lv_font_arial_18.c

File diff suppressed because it is too large

2497
src/fonts/lv_font_arial_20.c

File diff suppressed because it is too large

2710
src/fonts/lv_font_arial_22.c

File diff suppressed because it is too large

3159
src/fonts/lv_font_arial_24.c

File diff suppressed because it is too large

3501
src/fonts/lv_font_arial_26.c

File diff suppressed because it is too large

3858
src/fonts/lv_font_arial_28.c

File diff suppressed because it is too large

4317
src/fonts/lv_font_arial_30.c

File diff suppressed because it is too large

4745
src/fonts/lv_font_arial_32.c

File diff suppressed because it is too large

5195
src/fonts/lv_font_arial_34.c

File diff suppressed because it is too large

5812
src/fonts/lv_font_arial_36.c

File diff suppressed because it is too large

6302
src/fonts/lv_font_arial_38.c

File diff suppressed because it is too large

6810
src/fonts/lv_font_arial_40.c

File diff suppressed because it is too large

7541
src/fonts/lv_font_arial_42.c

File diff suppressed because it is too large

8210
src/fonts/lv_font_arial_44.c

File diff suppressed because it is too large

9797
src/fonts/lv_font_arial_48.c

File diff suppressed because it is too large

10460
src/fonts/lv_font_arial_50.c

File diff suppressed because it is too large

13677
src/fonts/lv_font_arial_58.c

File diff suppressed because it is too large

17268
src/fonts/lv_font_arial_66.c

File diff suppressed because it is too large

21373
src/fonts/lv_font_arial_74.c

File diff suppressed because it is too large

1059
src/fonts/lv_font_arial_8.c

File diff suppressed because it is too large

5
src/helpers/BaseChatMesh.cpp

@ -139,7 +139,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
packet->header = save;
}
bool is_new = false; // true = not in contacts[], false = exists in contacts[]
bool is_new = false; // true = newly discovered (not previously in contacts[]), false = existing contact
if (from == NULL) {
if (!shouldAutoAddContactType(parser.getType())) {
ContactInfo ci;
@ -166,10 +166,11 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact");
return;
}
populateContactFromAdvert(*from, id, parser, timestamp);
from->sync_since = 0;
from->shared_secret_valid = false;
is_new = true; // slot was just allocated for a brand-new contact
}
// update

20
src/helpers/CommonCLI.cpp

@ -230,15 +230,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
_callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first
strcpy(reply, "OK - Advert sent");
} else if (memcmp(command, "clock sync", 10) == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (sender_timestamp > curr) {
//uint32_t curr = getRTCClock()->getCurrentTime();
//if (sender_timestamp > curr) {
getRTCClock()->setCurrentTime(sender_timestamp + 1);
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else {
strcpy(reply, "ERR: clock cannot go backwards");
}
//} else {
// strcpy(reply, "ERR: clock cannot go backwards");
//}
} else if (memcmp(command, "start ota", 9) == 0) {
if (!_board->startOTAUpdate(_prefs->node_name, reply)) {
strcpy(reply, "Error");
@ -249,15 +249,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds)
uint32_t secs = _atoi(&command[5]);
uint32_t curr = getRTCClock()->getCurrentTime();
if (secs > curr) {
//int32_t curr = getRTCClock()->getCurrentTime();
//if (secs > curr) {
getRTCClock()->setCurrentTime(secs);
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(reply, "OK - clock set: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else {
strcpy(reply, "(ERR: clock cannot go backwards)");
}
//} else {
// strcpy(reply, "(ERR: clock cannot go backwards)");
//}
} else if (memcmp(command, "neighbors", 9) == 0) {
_callbacks->formatNeighborsReply(reply);
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {

112
src/helpers/esp32/ESPNOWRadio.cpp

@ -3,12 +3,53 @@
#include <WiFi.h>
#include <esp_wifi.h>
static uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static esp_now_peer_info_t peerInfo;
static volatile bool is_send_complete = false;
static esp_err_t last_send_result;
static uint8_t rx_buf[256];
static uint8_t last_rx_len = 0;
static uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static esp_now_peer_info_t peerInfo;
static volatile bool is_send_complete = false;
static esp_err_t last_send_result;
static uint8_t rx_buf[256];
static uint8_t last_rx_len = 0;
static char bridge_secret[16];
#ifdef CLIENT_WITHOUT_LORA
static constexpr uint16_t BRIDGE_PACKET_MAGIC = 0xC03E;
static const size_t MAX_ESPNOW_PACKET_SIZE = 250;
/**
* @brief Common field sizes used by bridge implementations
*
* These constants define the size of common packet fields used across bridges.
* BRIDGE_MAGIC_SIZE is used by all bridges for packet identification.
* BRIDGE_LENGTH_SIZE is used by bridges that need explicit length fields (like RS232).
* BRIDGE_CHECKSUM_SIZE is used by all bridges for Fletcher-16 checksums.
*/
static constexpr uint16_t BRIDGE_MAGIC_SIZE = sizeof(BRIDGE_PACKET_MAGIC);
static constexpr uint16_t BRIDGE_LENGTH_SIZE = sizeof(uint16_t);
static constexpr uint16_t BRIDGE_CHECKSUM_SIZE = sizeof(uint16_t);
static void xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = strlen(bridge_secret);
for (size_t i = 0; i < len; i++) {
data[i] ^= bridge_secret[i % keyLen];
}
}
static uint16_t fletcher16(const uint8_t *data, size_t len) {
uint8_t sum1 = 0, sum2 = 0;
for (size_t i = 0; i < len; i++) {
sum1 = (sum1 + data[i]) % 255;
sum2 = (sum2 + sum1) % 255;
}
return (sum2 << 8) | sum1;
}
static bool validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum) {
uint16_t calculated_checksum = fletcher16(data, len);
return received_checksum == calculated_checksum;
}
#endif
// callback when data is sent
static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
@ -18,13 +59,38 @@ static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
ESPNOW_DEBUG_PRINTLN("Recv: len = %d", len);
memcpy(rx_buf, data, len);
last_rx_len = len;
#ifdef CLIENT_WITHOUT_LORA
if (len < 4) return;
uint16_t magic = (data[0] << 8) | data[1];
if (magic != BRIDGE_PACKET_MAGIC) return;
uint8_t decrypted[MAX_ESPNOW_PACKET_SIZE];
int encLen = len - 2; // everything except magic
memcpy(decrypted, data + 2, encLen);
xorCrypt(decrypted, encLen);
uint16_t rx_cksum = (decrypted[0] << 8) | decrypted[1];
uint8_t* meshPayload = decrypted + 2;
int meshLen = encLen - 2;
if (!validateChecksum(meshPayload, meshLen, rx_cksum)) return;
memcpy(rx_buf, meshPayload, meshLen);
last_rx_len = meshLen;
#else
memcpy(rx_buf, data, len);
last_rx_len = len;
#endif
}
void ESPNOWRadio::init() {
// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
#ifdef CLIENT_WITHOUT_LORA
strncpy(bridge_secret, "LVSITANOS", sizeof(bridge_secret));
#endif
// Long Range mode
esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR);
@ -75,13 +141,39 @@ uint32_t ESPNOWRadio::intID() {
bool ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) {
// Send message via ESP-NOW
is_send_complete = false;
esp_err_t result = esp_now_send(broadcastAddress, bytes, len);
#ifdef CLIENT_WITHOUT_LORA
if (!bytes || len <= 0) {
ESPNOW_DEBUG_PRINTLN("Invalid raw mesh packet");
return false;
}
if (len > (MAX_ESPNOW_PACKET_SIZE - 4)) {
ESPNOW_DEBUG_PRINTLN("Mesh packet too large for ESP-NOW");
return false;
}
uint8_t out[MAX_ESPNOW_PACKET_SIZE];
out[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF;
out[1] = BRIDGE_PACKET_MAGIC & 0xFF;
uint16_t cksum = fletcher16(bytes, len);
out[2] = (cksum >> 8) & 0xFF;
out[3] = cksum & 0xFF;
memcpy(out + 4, bytes, len);
xorCrypt(out + 2, len + 2);
esp_err_t result = esp_now_send(broadcastAddress, out, len + 4);
#else
is_send_complete = false;
esp_err_t result = esp_now_send(broadcastAddress, bytes, len);
#endif
if (result == ESP_OK) {
n_sent++;
ESPNOW_DEBUG_PRINTLN("Send success");
return true;
}
last_send_result = result;
is_send_complete = true;
ESPNOW_DEBUG_PRINTLN("Send failed: %d", result);

307
src/helpers/esp32/SenseCapHAL.h

@ -0,0 +1,307 @@
#pragma once
// SenseCapHAL - Custom RadioLib 7.x HAL for the Seeed SenseCAP Indicator
//
// The SenseCAP Indicator routes SX1262 control pins through a TCA9535
// 16-bit I2C IO expander (address 0x20) because GPIOs 0-15 are used by
// the 16-bit RGB display bus.
//
// Pin encoding matches LovyanGFX convention: (pin_index | IO_EXPANDER)
// IO_EXPANDER = 0x40 (defined as build flag in platformio.ini)
//
// TCA9535 Port 0 pin assignments:
// pin 0 = NSS (EXPANDER_IO_RADIO_NSS) — SX1262 chip-select
// pin 1 = RESET (EXPANDER_IO_RADIO_RST) — SX1262 hardware reset
// pin 2 = BUSY (EXPANDER_IO_RADIO_BUSY) — SX1262 busy signal
// pin 3 = DIO1 (EXPANDER_IO_RADIO_DIO_1) — SX1262 IRQ
// pin 4 = display SPI CS — ST7701 chip-select (must stay OUTPUT HIGH)
// pin 5 = display RESX — ST7701 hardware reset (LovyanGFX cfg.pin_rst)
// pin 6 = touch INT — FT5x06 interrupt (input, not used in SW)
// pin 7 = touch RST — FT5x06 reset (not used in SW)
//
// DIO1 interrupt: TCA9535 /INT output → GPIO 42 (IO_EXPANDER_IRQ)
// The /INT pin fires FALLING when any input changes.
//
// TCA9535 register map (Port 0 only):
// 0x00 Input Port 0 (read)
// 0x02 Output Port 0 (write; 1=HIGH, 0=LOW)
// 0x06 Config Port 0 (0=output, 1=input)
#include <RadioLib.h>
#include <Wire.h>
#include <SPI.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <driver/gpio.h>
class SenseCapHAL : public ArduinoHal {
TwoWire* _wire;
uint8_t _addr; // 7-bit I2C address of TCA9535 (0x20)
uint8_t _out0; // cached Output Port 0 latch (all HIGH = de-asserted)
uint8_t _cfg0; // cached Config Port 0 (all 1 = input initially)
int _sclk, _miso, _mosi; // SPI data pins
SemaphoreHandle_t _mutex; // shared Wire mutex (set via setMutex() after creation)
// ── Deferred IRQ dispatch ──────────────────────────────────────────────
// The TCA9535 /INT fires for ANY input change on Port 0, not just DIO1.
// BUSY transitions, touch-INT (pin 6), and other inputs all trigger GPIO 42.
// We cannot do I2C inside an ISR, so the raw ISR just sets a flag.
// dispatchPendingIrq() (called from the main loop via recvRaw) reads Port 0
// to verify DIO1 is actually HIGH before forwarding the event to RadioLib.
inline static volatile bool _s_pending = false;
inline static void (*_s_cb)(void) = nullptr;
uint8_t _irqBit = 0; // Port 0 bit-mask for DIO1, set in attachInterrupt()
// NOTE: no IRAM_ATTR — class-static IRAM functions cause Xtensa literal-pool
// linker errors when defined inline in a header. Running from flash is fine
// here: the SenseCAP uses ESP32-S3 with no mid-operation flash cache disables,
// and LoRa packet timescales (tens of ms) dwarf any flash-cache miss latency.
static void _rawIsr() { _s_pending = true; }
// A pin is on the expander when its upper nibble equals IO_EXPANDER
static bool isExp(uint32_t pin) {
return (pin & ~0x0Fu) == (uint32_t)IO_EXPANDER;
}
// Bit mask within TCA9535 port register
static uint8_t expBit(uint32_t pin) {
return (uint8_t)(1u << (pin & 0x07u));
}
// Pins 0-7 are Port 0; pins 8-15 are Port 1
static bool isPort0(uint32_t pin) {
return (pin & 0x08u) == 0;
}
void writeReg(uint8_t reg, uint8_t val) {
if (_mutex) xSemaphoreTake(_mutex, portMAX_DELAY);
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->write(val);
_wire->endTransmission();
if (_mutex) xSemaphoreGive(_mutex);
}
uint8_t readReg(uint8_t reg) {
if (_mutex) xSemaphoreTake(_mutex, portMAX_DELAY);
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->endTransmission(false);
_wire->requestFrom(_addr, (uint8_t)1);
uint8_t val = _wire->available() ? _wire->read() : 0xFF;
if (_mutex) xSemaphoreGive(_mutex);
return val;
}
public:
SenseCapHAL(SPIClass& spi, int sclk, int miso, int mosi,
uint8_t i2c_addr = 0x20, TwoWire* wire = &Wire)
: ArduinoHal(spi, SPISettings(2000000, MSBFIRST, SPI_MODE0))
, _wire(wire), _addr(i2c_addr)
, _out0(0xFF) // all HIGH (NSS, RESET de-asserted)
, _cfg0(0xFF) // all inputs initially
, _sclk(sclk), _miso(miso), _mosi(mosi)
, _mutex(nullptr)
{}
// Set the shared I2C mutex. Must be called before any Wire operations.
// Both this HAL and the touch-read callback must use the same handle.
void setMutex(SemaphoreHandle_t m) { _mutex = m; }
// Call once Wire is running, AFTER lcd.begin() has completed.
// Configures TCA9535 Port 0 radio pins (bits 0-3) and locks bit 4 (display CS).
// Bits 5-7 are preserved exactly as LovyanGFX left them.
void initExpander() {
// Read the current register values set by LovyanGFX
uint8_t cur_out = readReg(0x02);
uint8_t cur_cfg = readReg(0x06);
// Bits 0-3 (radio): inputs initially; RadioLib reconfigures as needed.
// Output latch HIGH = de-asserted.
// Bit 4 (display CS): OUTPUT HIGH — permanently de-asserted.
// LovyanGFX leaves this as INPUT after lcd.begin();
// a floating CS allows radio SPI to corrupt the ST7701.
// Bit 5 (display RESX): preserved as OUTPUT HIGH — lcd.begin() already pulsed
// it LOW→HIGH to reset the ST7701 before init commands.
// Bits 6-7 (touch INT/RST): preserved as LovyanGFX left them (inputs).
_out0 = (cur_out & 0xE0) | 0x1F; // bits 0-4 = HIGH, bits 5-7 = preserved
_cfg0 = (cur_cfg & 0xE0) | 0x0F; // bits 0-3 = input, bit 4 = OUTPUT, bits 5-7 = preserved
writeReg(0x02, _out0); // Output Port 0
writeReg(0x06, _cfg0); // Config Port 0
// Port 1 (pins 8-15) — appears unused on the SenseCAP Indicator.
// TCA9535 /INT fires on ANY input change on either port and only clears
// when the triggering port register is read. Floating Port 1 inputs
// continuously re-assert /INT, preventing DIO1 edges from ever firing.
// Fix: make all Port 1 pins OUTPUT HIGH so they can never float.
writeReg(0x03, 0xFF); // Output Port 1: all HIGH
writeReg(0x07, 0x00); // Config Port 1: all OUTPUTS
// Read both input ports now to establish a clean /INT baseline.
readReg(0x00);
readReg(0x01);
Serial.printf("[SenseCapHAL] TCA9535@0x%02X init: out0=0x%02X cfg0=0x%02X\n",
_addr, _out0, _cfg0);
}
// ── Overrides ──────────────────────────────────────────────────────────
//
// SOFTWARE (bit-bang) SPI — intentionally avoids the hardware FSPI peripheral.
//
// Calling SPI.begin() (hardware FSPI) AFTER lcd.begin() disrupts the
// LCD_CAM peripheral that is already running the RGB parallel bus,
// causing the display to go blank. Since the LCD_CAM uses GPIOs 0–21
// and the SX1262 SPI uses GPIOs 41/47/48, there is no pin conflict —
// the problem is at the peripheral / DMA level when FSPI is initialised
// while LCD_CAM is active.
//
// Bit-bang SPI on the same GPIOs avoids hardware SPI entirely.
// The SX1262 requires at most ~16 MHz SPI; bit-bang on ESP32-S3 gives
// ~1–2 MHz which is more than adequate for LoRa configuration and DIO
// interrupt latency is unaffected.
// ArduinoHal::init() only calls spiBegin() when initInterface==true,
// which is false when an explicit SPIClass& is supplied. Override to
// always call our spiBegin().
void init() override { spiBegin(); }
void spiBegin() override {
// Configure GPIO pins for direct CPU control (disconnects any
// peripheral / GPIO-matrix function, including leftover FSPI routing).
::pinMode(_sclk, OUTPUT);
::pinMode(_mosi, OUTPUT);
::pinMode(_miso, INPUT);
::digitalWrite(_sclk, LOW);
Serial.printf("[SenseCapHAL] SPI (bit-bang) SCLK=%d MOSI=%d MISO=%d\n",
_sclk, _mosi, _miso);
}
void spiEnd() override {
// nothing to tear down for bit-bang
}
void spiBeginTransaction() override {
// nothing needed — CS is managed via the expander in digitalWrite()
}
void spiEndTransaction() override {
// nothing needed
}
// Bit-bang SPI Mode 0 (CPOL=0, CPHA=0): data sampled on rising edge.
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
for (size_t i = 0; i < len; i++) {
uint8_t tx = out ? out[i] : 0x00;
uint8_t rx = 0;
for (int b = 7; b >= 0; b--) {
::digitalWrite(_mosi, (tx >> b) & 1);
::digitalWrite(_sclk, HIGH);
rx = (rx << 1) | (::digitalRead(_miso) ? 1 : 0);
::digitalWrite(_sclk, LOW);
}
if (in) in[i] = rx;
}
}
void pinMode(uint32_t pin, uint32_t mode) override {
if (isExp(pin) && isPort0(pin)) {
if (mode == OUTPUT) _cfg0 &= ~expBit(pin);
else _cfg0 |= expBit(pin);
writeReg(0x06, _cfg0);
} else {
ArduinoHal::pinMode(pin, mode);
}
}
void digitalWrite(uint32_t pin, uint32_t value) override {
if (isExp(pin) && isPort0(pin)) {
if (value) _out0 |= expBit(pin);
else _out0 &= ~expBit(pin);
writeReg(0x02, _out0);
} else {
ArduinoHal::digitalWrite(pin, value);
}
}
uint32_t digitalRead(uint32_t pin) override {
if (isExp(pin) && isPort0(pin)) {
return (readReg(0x00) & expBit(pin)) ? 1 : 0;
}
return ArduinoHal::digitalRead(pin);
}
// DIO1 is expander pin 3; redirect the interrupt to IO_EXPANDER_IRQ (GPIO 42).
// TCA9535 /INT fires FALLING when any input changes (DIO1 going HIGH = packet ready).
// RadioLib 7.x calls pinToInterrupt(irq_pin) BEFORE attachInterrupt().
// Without this override, pinToInterrupt(0x43) → digitalPinToInterrupt(67)
// which is invalid on ESP32-S3, so attachInterrupt() silently fails.
// Return IO_EXPANDER_IRQ (GPIO 42) directly for any expander pin.
uint32_t pinToInterrupt(uint32_t pin) override {
if (isExp(pin)) {
return (uint32_t)IO_EXPANDER_IRQ;
}
return ArduinoHal::pinToInterrupt(pin);
}
// Called with either:
// a) raw expander pin (e.g. 0x43) — direct call path
// b) IO_EXPANDER_IRQ (42) — via pinToInterrupt() path (RadioLib 7.x)
//
// DEFERRED DISPATCH: instead of calling `cb` (= RadioLib's setFlag) directly
// from the ISR, we attach a lightweight raw ISR that only sets _s_pending.
// The real dispatch (reading Port 0 to verify DIO1 is HIGH) happens later
// in dispatchPendingIrq(), called from the main loop via recvRaw().
// This prevents touch-INT (pin 6), BUSY, and other Port 0 input changes
// from triggering false radio-interrupt events.
void attachInterrupt(uint32_t pin, void (*cb)(void), uint32_t mode) override {
if (isExp(pin) || pin == (uint32_t)IO_EXPANDER_IRQ) {
_s_cb = cb; // save RadioLib's setFlag callback
_irqBit = isExp(pin) ? expBit(pin) // direct expander pin → derive bit
: (uint8_t)(1u << 3u); // pin=42 path → DIO1 is bit 3
// Clear any pending TCA9535 /INT by reading BOTH port registers.
readReg(0x00);
readReg(0x01);
::pinMode(IO_EXPANDER_IRQ, INPUT_PULLUP);
int gpio42 = ::digitalRead(IO_EXPANDER_IRQ);
::attachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ), _rawIsr, FALLING);
Serial.printf("[SenseCapHAL] DIO1 interrupt → GPIO %d state=%d (FALLING, deferred)\n",
IO_EXPANDER_IRQ, gpio42);
} else {
ArduinoHal::attachInterrupt(pin, cb, mode);
}
}
// ── dispatchPendingIrq ────────────────────────────────────────────────
// Call from the main loop (NOT from ISR). Reads Port 0 to check whether
// DIO1 is actually HIGH. If so, forwards the event to RadioLib (setFlag).
// Ignores spurious triggers from BUSY, touch-INT, or any other Port 0 input.
void dispatchPendingIrq() {
if (!_s_pending) return;
_s_pending = false;
// Reading Port 0 also acknowledges the TCA9535 /INT for ALL inputs,
// including touch INT and BUSY — preventing /INT from staying stuck LOW.
uint8_t port0 = readReg(0x00);
if (_irqBit && (port0 & _irqBit)) {
// DIO1 is HIGH → this is a real radio interrupt (RX_DONE or TX_DONE)
if (_s_cb) _s_cb();
}
// else: spurious — BUSY transition, touch INT, etc. Do nothing.
}
void detachInterrupt(uint32_t pin) override {
if (isExp(pin) || pin == (uint32_t)IO_EXPANDER_IRQ) {
::detachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ));
} else {
ArduinoHal::detachInterrupt(pin);
}
}
// Scan for the TCA9535 on I2C and log the result
bool scanExpander() {
if (_mutex) xSemaphoreTake(_mutex, portMAX_DELAY);
_wire->beginTransmission(_addr);
bool found = (_wire->endTransmission() == 0);
if (_mutex) xSemaphoreGive(_mutex);
return found;
}
};

27
src/helpers/esp32/SenseCapSX1262Wrapper.h

@ -0,0 +1,27 @@
#pragma once
// SenseCapSX1262Wrapper — extends CustomSX1262Wrapper with a DIO1-verified
// dispatchPendingIrq() override.
//
// On SenseCAP Indicator the SX1262 DIO1, BUSY, and touch-INT all share the
// same TCA9535 /INT line (GPIO 42). The raw ISR in SenseCapHAL only sets a
// pending flag; this override reads Port 0 via I2C and calls setFlag() only
// when DIO1 is actually HIGH, silencing spurious triggers from BUSY
// transitions, touch events, or any other Port 0 input change.
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include "SenseCapHAL.h"
class SenseCapSX1262Wrapper : public CustomSX1262Wrapper {
public:
SenseCapSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board)
: CustomSX1262Wrapper(radio, board) {}
protected:
void dispatchPendingIrq() override {
// Access HAL via Module (Module::hal is public in RadioLib 7.x)
auto* hal = static_cast<SenseCapHAL*>(
static_cast<CustomSX1262*>(_radio)->getMod()->hal);
hal->dispatchPendingIrq();
}
};

21
src/helpers/radiolib/RadioLibWrappers.cpp

@ -57,7 +57,11 @@ void RadioLibWrapper::idle() {
void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
_threshold = threshold;
if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling
// Only restart sampling when threshold > 0 (interference detection enabled).
// With threshold=0 the noise floor is never used, so periodic SPI re-sampling
// is pointless — and on boards with a shared-bus IRQ (e.g. TCA9535) the BUSY
// transitions generated by those SPI calls fire spurious interrupts that break RX.
if (threshold > 0 && _num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) {
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
@ -117,9 +121,15 @@ bool RadioLibWrapper::isInRecvMode() const {
}
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
// Allow subclasses to gate STATE_INT_READY on the physical IRQ pin.
// On boards with a shared-bus /INT (e.g. TCA9535), this reads DIO1
// via I2C and only sets STATE_INT_READY when the radio actually asserted it.
dispatchPendingIrq();
int len = 0;
if (state & STATE_INT_READY) {
len = _radio->getPacketLength();
MESH_DEBUG_PRINTLN("RadioLibWrapper: IRQ fired, pkt_len=%d", len);
if (len > 0) {
if (len > sz) { len = sz; }
int err = _radio->readData(bytes, len);
@ -128,7 +138,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
len = 0;
n_recv_errors++;
} else {
// Serial.print(" readData() -> "); Serial.println(len);
MESH_DEBUG_PRINTLN("RadioLibWrapper: recv %d bytes RSSI=%.1f SNR=%.1f", len, getLastRSSI(), getLastSNR());
n_recv++;
}
}
@ -136,9 +146,16 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
}
if (state != STATE_RX) {
// Note: we intentionally do NOT call isReceivingPacket() here.
// On boards where DIO1/BUSY share a TCA9535 /INT line, isReceivingPacket()
// does an SPI read which generates another BUSY pulse → another spurious
// interrupt → infinite pkt_len=0 loop. Calling startReceive() unconditionally
// is safe: if pkt_len was 0 the packet was not yet complete (or the interrupt
// was fully spurious), so restarting RX is correct.
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
state &= ~STATE_INT_READY; // BUSY transitions during startReceive fire TCA9535 /INT spuriously
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
}

8
src/helpers/radiolib/RadioLibWrappers.h

@ -19,6 +19,14 @@ protected:
virtual bool isReceivingPacket() =0;
virtual void doResetAGC();
// Called at the start of recvRaw() before checking STATE_INT_READY.
// Override on boards where the radio IRQ line is shared with other inputs
// (e.g. TCA9535 Port 0 also carries BUSY and touch INT). The override
// must read the physical IRQ pin and, ONLY if the radio has actually
// asserted it, call setFlag() to set STATE_INT_READY. Default: no-op
// (interrupt-driven boards set STATE_INT_READY directly in the ISR).
virtual void dispatchPendingIrq() {}
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }

2
variants/generic-e22/variant.h

@ -19,7 +19,7 @@
// Radio
#define USE_SX1262 // E22-900M30S uses SX1262
#define USE_SX1268 // E22-400M30S uses SX1268
#define SX126X_MAX_POWER 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA)
#define SX126X_MAX_POWER 27 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA)
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V
#define SX126X_CS 18 // EBYTE module's NSS pin

2
variants/meshadventurer/variant.h

@ -19,7 +19,7 @@
// Radio
#define USE_SX1262 // E22-900M30S uses SX1262
#define USE_SX1268 // E22-400M30S uses SX1268
#define SX126X_MAX_POWER 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA)
#define SX126X_MAX_POWER 27 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA)
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V
#define SX126X_CS 18 // EBYTE module's NSS pin

BIN
variants/sensecap_indicator-espnow/1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
variants/sensecap_indicator-espnow/2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

BIN
variants/sensecap_indicator-espnow/3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

BIN
variants/sensecap_indicator-espnow/4.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

45
variants/sensecap_indicator-espnow/SCIndicatorDisplay.h

@ -8,9 +8,35 @@
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
// Custom ST7701 panel subclass with SenseCap Indicator-specific init commands.
// The default Panel_ST7701 init sequence does not set MADCTL or SDIR, so the
// display outputs pixels in an orientation that doesn't match the physical
// RGB bus wiring on the SenseCap Indicator — resulting in a blank screen.
// These extra commands (ported from Meshtastic's LGFX_INDICATOR.h) apply the
// required vertical flip (MADCTL) and horizontal flip (SDIR) after the
// standard init sequence.
class Panel_SCIndicator : public lgfx::Panel_ST7701
{
public:
const uint8_t *getInitCommands(uint8_t listno) const override
{
static constexpr const uint8_t list1[] = {
0x36, 1, 0x10, // MADCTL: vertical flip
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL
0xC7, 1, 0x04, // SDIR: horizontal flip
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS
0xFF, 0xFF
};
switch (listno) {
case 1: return list1;
default: return lgfx::Panel_ST7701::getInitCommands(listno);
}
}
};
class LGFX : public lgfx::LGFX_Device
{
lgfx::Panel_ST7701 _panel_instance;
Panel_SCIndicator _panel_instance;
lgfx::Bus_RGB _bus_instance;
lgfx::Light_PWM _light_instance;
lgfx::Touch_FT5x06 _touch_instance;
@ -31,13 +57,16 @@ public:
cfg.panel_height = screenHeight;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 1;
cfg.offset_rotation = 0;
// pin_rst stays at default (no-connect). Upstream LovyanGFX cannot
// toggle expander pins. The ST7701 RESX (TCA9535 bit 5) is pulsed
// manually by sensecap_lcd_reset_pulse() in main.cpp before lcd.begin().
_panel_instance.config(cfg);
}
{
auto cfg = _panel_instance.config_detail();
cfg.pin_cs = 4 | IO_EXPANDER;
cfg.pin_cs = 4 | IO_EXPANDER; // TCA9535 bit 4 = ST7701 chip-select
cfg.pin_sclk = 41;
cfg.pin_mosi = 48;
cfg.use_psram = 1;
@ -48,7 +77,7 @@ public:
auto cfg = _bus_instance.config();
cfg.panel = &_panel_instance;
cfg.freq_write = 8000000;
cfg.freq_write = 6000000; // 6 MHz — matches Meshtastic LGFX_INDICATOR
cfg.pin_henable = 18;
cfg.pin_pclk = 21;
@ -103,10 +132,10 @@ public:
cfg.x_max = 479;
cfg.y_min = 0;
cfg.y_max = 479;
cfg.pin_int = GPIO_NUM_NC;
cfg.pin_rst = GPIO_NUM_NC;
cfg.bus_shared = true;
cfg.offset_rotation = 0;
cfg.pin_int = GPIO_NUM_NC; // do NOT use IO_EXPANDER for touch pins
cfg.pin_rst = GPIO_NUM_NC; // do NOT use IO_EXPANDER for touch pins
cfg.bus_shared = false;
cfg.offset_rotation = 2; // 180° — mirrors Meshtastic bbct.setOrientation(180,480,480)
cfg.i2c_port = 0;
cfg.i2c_addr = 0x48;

35
variants/sensecap_indicator-espnow/platformio.ini

@ -6,7 +6,7 @@ board_build.flash_mode = qio
board_build.psram_type = opi
board_upload.flash_size = 8MB
board_upload.maximum_size = 8388608
board_build.partitions = default.csv
board_build.partitions = partition-table-8MB.csv
build_flags =
${esp32_base.build_flags}
-D PIN_BOARD_SDA=39
@ -36,15 +36,40 @@ lib_deps=${esp32_base.lib_deps}
extends =SenseCapIndicator-ESPNow
build_flags =
${SenseCapIndicator-ESPNow.build_flags}
-I examples/companion_radio/ui-new
; -I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D CORE_DEBUG_LEVEL=4 ; 0: None, 1: Error, 2: Warn, 3: Info, 4: Debug, 5: Verbose
-D LV_CONF_PATH=lv_conf.h
-D SEEED_SENSECAP_INDICATOR
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=SenseCapSX1262Wrapper
-D PIN_USER_BTN=38
-D LANG_EN
-D LORA_FREQ=869.525
-D LORA_BW=250.0
-D LORA_SF=11
-D LORA_CR=5
-D LORA_TX_POWER=20
-D SX126X_DIO3_TCXO_VOLTAGE=2.4
-D SX126X_DIO2_AS_RF_SWITCH=true
-D ADVERT_NAME='"SenseCap Client"'
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=8
-D MESH_DEBUG=1
-D RADIOLIB_SPI_PARANOID=0
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
; NOTE: DO NOT ENABLE --> -D ESPNOW_DEBUG_LOGGING=1
build_src_filter = ${SenseCapIndicator-ESPNow.build_src_filter}
+<../examples/companion_radio/ui-new/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/simple_secure_chat_ui/*.cpp>
+<fonts/*.c>
+<UI/*.c>
lib_deps =
${SenseCapIndicator-ESPNow.lib_deps}
densaugeo/base64 @ ~1.4.0
fbiego/ESP32Time@^2.0.6
lvgl/[email protected]
lovyan03/LovyanGFX@^1.2.7
bitbank2/PNGdec@^1.1.6
densaugeo/base64 @ ~1.4.0

181
variants/sensecap_indicator-espnow/target.cpp

@ -1,44 +1,169 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <driver/gpio.h>
// Recover a stuck I2C bus by manually clocking SCL up to 9 times until
// SDA is released, then issuing a STOP condition.
static void i2c_bus_recovery() {
Serial.println("[i2c_recover] Attempting I2C bus recovery...");
pinMode(PIN_BOARD_SCL, OUTPUT_OPEN_DRAIN);
pinMode(PIN_BOARD_SDA, INPUT_PULLUP);
digitalWrite(PIN_BOARD_SCL, HIGH);
delayMicroseconds(10);
bool released = false;
for (int i = 0; i < 9; i++) {
digitalWrite(PIN_BOARD_SCL, LOW);
delayMicroseconds(10);
digitalWrite(PIN_BOARD_SCL, HIGH);
delayMicroseconds(10);
if (digitalRead(PIN_BOARD_SDA) == HIGH) {
released = true;
Serial.printf("[i2c_recover] SDA released after %d clocks\n", i + 1);
break;
}
}
if (!released) {
Serial.println("[i2c_recover] SDA still LOW after 9 clocks!");
}
// Generate STOP condition: SDA LOW→HIGH while SCL HIGH
pinMode(PIN_BOARD_SDA, OUTPUT_OPEN_DRAIN);
digitalWrite(PIN_BOARD_SDA, LOW);
delayMicroseconds(10);
digitalWrite(PIN_BOARD_SCL, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_BOARD_SDA, HIGH);
delayMicroseconds(10);
// Restore pins to input so Wire can take over
pinMode(PIN_BOARD_SCL, INPUT_PULLUP);
pinMode(PIN_BOARD_SDA, INPUT_PULLUP);
delay(5);
Serial.println("[i2c_recover] Done.");
}
ESP32Board board;
ESPNOWRadio radio_driver;
// ── SX1262 SPI data pins (hardware FSPI / SPI2) ────────────────────────
// Shared with the ST7701 display config SPI (bit-banged by LovyanGFX only
// during lcd.begin(), so no runtime conflict).
#define LORA_SCLK 41
#define LORA_MISO 47
#define LORA_MOSI 48
// ── SX1262 control pins via TCA9535 I2C IO Expander (7-bit addr 0x20) ──
// Encoded as (pin_index | IO_EXPANDER) per LovyanGFX / Seeed convention.
// IO_EXPANDER = 0x40 and IO_EXPANDER_IRQ = 42 defined in platformio.ini
#define LORA_NSS (0 | IO_EXPANDER) // TCA9535 port-0 pin 0 (0x40)
#define LORA_RESET (1 | IO_EXPANDER) // TCA9535 port-0 pin 1 (0x41)
#define LORA_BUSY (2 | IO_EXPANDER) // TCA9535 port-0 pin 2 (0x42)
#define LORA_DIO1 (3 | IO_EXPANDER) // TCA9535 port-0 pin 3 (0x43)
// → interrupt fires on GPIO 42
ESP32RTCClock rtc_clock;
#if defined(ENV_INCLUDE_GPS)
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, (mesh::RTCClock*)&rtc_clock);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors = EnvironmentSensorManager();
#endif
// Custom RadioLib HAL: routes expander pins through TCA9535 (I2C 0x20).
// Uses software (bit-bang) SPI on SCLK/MOSI/MISO to avoid taking the
// hardware FSPI peripheral, which would disrupt LovyanGFX's LCD_CAM output.
// SPI is passed as a placeholder only — all SPI methods are overridden.
static SenseCapHAL radio_hal(SPI, LORA_SCLK, LORA_MISO, LORA_MOSI, 0x20, &Wire);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#ifdef PIN_USER_BTN
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
#endif
#endif
// SX1262 module using the custom HAL
RADIO_CLASS radio = new Module(&radio_hal, LORA_NSS, LORA_DIO1, LORA_RESET, LORA_BUSY);
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
EnvironmentSensorManager sensors;
// Shared Wire mutex — created in radio_init(), exported via target.h
SemaphoreHandle_t g_i2c_mutex = nullptr;
// ── radio_init ─────────────────────────────────────────────────────────
bool radio_init() {
rtc_clock.begin();
// Create the shared Wire mutex FIRST so both the radio HAL and the
// LVGL touch callback (my_touchpad_read) serialise their Wire access.
g_i2c_mutex = xSemaphoreCreateMutex();
radio_hal.setMutex(g_i2c_mutex);
radio_driver.init();
Serial.println("[radio_init] Starting...");
Serial.printf("[radio_init] SPI : SCLK=%d MISO=%d MOSI=%d\n",
LORA_SCLK, LORA_MISO, LORA_MOSI);
Serial.printf("[radio_init] Expander (0x20): NSS=pin%d RST=pin%d BUSY=pin%d DIO1=pin%d\n",
LORA_NSS & 0x0F, LORA_RESET & 0x0F, LORA_BUSY & 0x0F, LORA_DIO1 & 0x0F);
Serial.printf("[radio_init] DIO1 interrupt → GPIO %d\n", IO_EXPANDER_IRQ);
return true; // success
}
// Take mutex for bus recovery + Wire re-init + scan
// (prevents LVGL touch task from accessing Wire concurrently)
xSemaphoreTake(g_i2c_mutex, portMAX_DELAY);
// Recover stuck I2C bus (may be left in bad state after display init)
i2c_bus_recovery();
// NOTE: as we are using the WiFi radio, the ESP_IDF will have enabled hardware RNG:
// https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/random.html
class ESP_RNG : public mesh::RNG {
public:
void random(uint8_t* dest, size_t sz) override {
esp_fill_random(dest, sz);
// Wire must be up before TCA9535 access — force full re-init after recovery
Wire.end();
delay(5);
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL, 400000);
Wire.setTimeOut(15); // 15 ms per address for fast scan
delay(10);
// ── Full I2C bus scan ───────────────────────────────────────────────────
Serial.println("[radio_init] I2C scan:");
uint8_t found_addr = 0;
for (uint8_t addr = 0x08; addr <= 0x77; addr++) {
Wire.beginTransmission(addr);
uint8_t err = Wire.endTransmission();
if (err == 0) {
Serial.printf("[radio_init] Device found at 0x%02X\n", addr);
found_addr = addr;
}
}
if (found_addr == 0) {
Serial.println("[radio_init] No I2C devices found!");
}
Serial.println("[radio_init] I2C scan done.");
xSemaphoreGive(g_i2c_mutex);
// Verify TCA9535 is present on the bus
if (radio_hal.scanExpander()) {
Serial.println("[radio_init] TCA9535 FOUND at 0x20");
} else {
Serial.println("[radio_init] WARNING: TCA9535 NOT FOUND at 0x20 – check I2C wiring");
}
};
// Set TCA9535 Port 0 defaults: all outputs HIGH (NSS/RESET de-asserted)
radio_hal.initExpander();
fallback_clock.begin();
Serial.println("[radio_init] RTC initialized");
// std_init() calls radio.begin() internally, which calls our spiBegin()
// to initialise the FSPI bus with pins 41/47/48, then communicates with
// the SX1262 using our expander-aware digitalWrite/digitalRead.
bool ok = radio.std_init();
Serial.printf("[radio_init] std_init result: %s\n", ok ? "OK" : "FAILED");
return ok;
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
ESP_RNG rng;
return mesh::LocalIdentity(&rng); // create new random identity
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}

60
variants/sensecap_indicator-espnow/target.h

@ -1,26 +1,78 @@
#pragma once
#include <Arduino.h>
#include <RadioLib.h>
#include <Wire.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <helpers/ESP32Board.h>
#include <helpers/esp32/ESPNOWRadio.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/esp32/SenseCapHAL.h> // TCA9535 IO expander HAL for RadioLib
#include <helpers/esp32/SenseCapSX1262Wrapper.h> // DIO1-verified IRQ dispatch
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
#endif
#ifdef DISPLAY_CLASS
#include "SCIndicatorDisplay.h"
#include <helpers/ui/MomentaryButton.h>
#endif
// -------------------------------------------------
// Board
// -------------------------------------------------
extern ESP32Board board;
extern ESPNOWRadio radio_driver;
extern ESP32RTCClock rtc_clock;
// -------------------------------------------------
// Radio (SX1262 - SenseCAP uses SX1262)
// -------------------------------------------------
extern SenseCapSX1262Wrapper radio_driver;
// -------------------------------------------------
// RTC
// -------------------------------------------------
extern AutoDiscoverRTCClock rtc_clock;
// -------------------------------------------------
// Sensors
// -------------------------------------------------
extern EnvironmentSensorManager sensors;
// -------------------------------------------------
// Display + Button
// -------------------------------------------------
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
// -------------------------------------------------
// Shared I2C mutex
// Protects Wire access shared between SenseCapHAL (TCA9535 @ 0x20)
// and the LVGL touch callback (FT5x06 @ 0x48).
// Created in radio_init() before std_init(); use via g_i2c_mutex.
// -------------------------------------------------
extern SemaphoreHandle_t g_i2c_mutex;
// -------------------------------------------------
// Functions
// -------------------------------------------------
bool radio_init();
mesh::LocalIdentity radio_new_identity();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();
Loading…
Cancel
Save