Solved: 10/16/2026; /* 10/14/2026
10/16/2026 - timing issues, some send fails, hopefully fixed
Arduino IDE version: 2.3.10
esp 32 Board version: 3.3.12
PSRAM: enabled
Preference reference: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
ESP32 Header: <esp_now.h>
The GFX2 UI example came from the author’s example.
NOTE: I found the minimum delay(1801) AFTER send message to SLAVE.
AND a delay(63) after Display: “invalidate(bounds());”
So far these DELAYS are working
delay(1801) 1801 milliseconds or 1.8 seconds
After touching a “button” on the display wait about 2 seconds or a send_fail will occur
if the result on the SLave does not take place a short wait and try button again.
Following code is currently working with ESP_NOW Slave:
#include <Arduino.h>
#include <Wire.h>
#include <stdio.h>
#include <Seeed_GFX.h>
#include <Seeed_UI.h> //included in GFX2
#include <string.h>
#include <esp_now.h>
#include <WiFi.h>
#include <esp_mac.h> // For the MAC2STR and MACSTR macros
Seeed_GFX display(Seeed_Product::Seeed_Round_Display_XIAO);
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3)
#error "Touch_UI targets XIAO ESP32S3. Select XIAO_ESP32S3 in Arduino IDE."
#endif
//MAC
uint8_t MAC1[] = {0x10, 0x91, 0xa8, 0x02, 0x5a, 0xac}; //XIAO-ESP32C3 "red 21"
typedef struct struct_message {
char a[16]; //could be increased if necessary
//int b; //These are not needed for sending button numbers via esp_now
//float c;
//bool d;
} struct_message;
struct_message sendData;
struct_message recvData;
esp_now_peer_info_t peerInfo;
//SCallback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
if(status == ESP_NOW_SEND_SUCCESS){
Serial.println("Send_Success");
}else{
Serial.println("Send_Fail");
}
}
//Callback when data is received
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *Data, int len) {
memcpy(&recvData, Data, sizeof(recvData));
Serial.print("Bytes received: ");
Serial.println(len);
Serial.println(recvData.a);
//Serial.println(recvData.b);
//Serial.println(recvData.c);
//Serial.println(recvData.d);
Serial.println("---------");
}
// *************** GFX2
constexpr uint16_t kBackground = 0x0004;
constexpr uint16_t kSurface = 0x10A3;
constexpr uint16_t kPadColor = 0x0841;
constexpr uint16_t kBorder = 0x2D7F;
constexpr uint16_t kPalette[] = {
TFT_CYAN, TFT_YELLOW, TFT_MAGENTA, TFT_GREEN, TFT_WHITE
};
const char* const kColorNames[] = {
"CYAN", "YELLOW", "MAGENTA", "GREEN", "WHITE"
};
constexpr size_t kPaletteSize = sizeof(kPalette) / sizeof(kPalette[0]);
constexpr size_t kMaxPaintPoints = 256;
enum CommandId : uint16_t {
CommandColor = 1,
CommandUndo,
CommandClear,
CommandB03, //
CommandB04, // #@! Button
CommandB08,
CommandB09,
CommandB12
};
struct PaintPoint {
int16_t x;
int16_t y;
uint16_t color;
bool startsStroke;
};
class PaintPad : public UiWidget {
public:
PaintPad(UiLabel& status, char\* statusText, size_t statusCapacity)
: UiWidget(10), \_status(status), \_statusText(statusText),
\_statusCapacity(statusCapacity) {}
void render(UiCanvas& canvas, const UiTheme&) override {
Seeed_GFX& gfx = canvas.gfx();
const UiRect& area = bounds();
int cx = display.width() / 2;
int cy = display.height() / 2;
gfx.drawCircle(cx, cy, 119, TFT_WHITE );
//int32_t x, int32_t y, int32_t r, uint32_t color
gfx.fillRoundRect(area.x, area.y, area.w, area.h, 12, kPadColor);
gfx.drawRoundRect(area.x, area.y, area.w, area.h, 12, kBorder);
if (\_count == 0) {
gfx.setTextDatum(MC_DATUM);
gfx.setTextColor(0x7BEF);
gfx.setTextSize(1);
gfx.drawString("DRAW HERE", area.x + area.w / 2,
area.y + area.h / 2);
} else {
for (size_t i = 0; i < \_count; ++i) {
const PaintPoint& point = \_points\[i\];
if (i && !point.startsStroke) {
const PaintPoint& previous = \_points\[i - 1\];
gfx.drawLine(previous.x, previous.y, point.x, point.y,
point.color);
}
gfx.fillCircle(point.x, point.y, 3, point.color);
}
}
// Current brush indicator, kept above the drawing.
gfx.fillCircle(area.x + area.w - 13, area.y + 13, 6,
kPalette\[\_colorIndex\]);
gfx.drawCircle(area.x + area.w - 13, area.y + 13, 7, TFT_WHITE);
}
bool onEvent(UiEvent& event) override {
const UiPoint position = {event.x, event.y};
if (event.type == UiEventType::PointerDown) {
if (!bounds().contains(position)) return false;
\_drawing = true;
addPoint(event.x, event.y, true);
return true;
}
if (event.type == UiEventType::PointerMove && \_drawing) {
if (bounds().contains(position)) addPoint(event.x, event.y, false);
return true;
}
if (event.type == UiEventType::PointerUp ||
event.type == UiEventType::PointerCancel) {
const bool handled = \_drawing;
\_drawing = false;
return handled;
}
return false;
}
void cycleColor() {
\_colorIndex = (\_colorIndex + 1) % kPaletteSize;
snprintf(\_statusText, \_statusCapacity, "BRUSH: %s",
kColorNames\[\_colorIndex\]);
\_status.setText(\_statusText);
Serial.println("pressed the COLOR Button\[7\]; send to espNOW");
Serial.println(kColorNames\[\_colorIndex\]);
invalidate(bounds());
delay(63);
// NOTE: Future: possible to use a color to SEND message to another PEER /SLAVE
strcpy(sendData.a, "B07");
//sendData.b = 7;
//sendData.c = 7.9;
//sendData.d = true;
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
}
void undo() {
Serial.println("pressed the UNDO Button \[6\]; send to espNOW");
delay(63);
strcpy(sendData.a, "B06");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
if (\_count == 0) {
setStatus("NOTHING TO UNDO");
return;
}
size_t strokeStart = \_count - 1;
while (strokeStart > 0 && !\_points\[strokeStart\].startsStroke)
--strokeStart;
\_count = strokeStart;
setStatus("LAST STROKE REMOVED");
invalidate(bounds());
delay(63);
}
void clear() {
delay(63);
Serial.println("pressed the Clear Button \[5\]; send to espNOW");
strcpy(sendData.a, "B05");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
\_count = 0;
\_drawing = false;
setStatus("CANVAS CLEARED");
invalidate(bounds());
}
// #@! Button
void B03() {
\_count = 0;
\_drawing = false;
setStatus("Pressed B03");
Serial.println("pressed the B03 Button; send to espNOW");
invalidate(bounds());
delay(63);
strcpy(sendData.a, "B03");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
}
void B04() {
\_count = 0;
\_drawing = false;
setStatus("Pressed B04");
Serial.println("pressed the B04 Button; send to espNOW");
invalidate(bounds());
delay(63);
strcpy(sendData.a, "B4");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
}
void B08() {
\_count = 0;
\_drawing = false;
setStatus("Pressed B08");
Serial.println("pressed the B08 Button; send to espNOW");
invalidate(bounds());
delay(63);
strcpy(sendData.a, "B08");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
}
void B09() {
\_count = 0;
\_drawing = false;
setStatus("Pressed B09");
Serial.println("pressed the B09 Button; send to espNOW");
invalidate(bounds());
delay(63);
strcpy(sendData.a, "B09");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
}
void B12() {
\_count = 0;
\_drawing = false;
setStatus("Pressed B12");
Serial.println("pressed the B12 Button; send to espNOW");
invalidate(bounds());
delay(63);
strcpy(sendData.a, "B12");
esp_err_t result = esp_now_send(MAC1, (uint8_t \*)&sendData, sizeof(sendData));
if (result == ESP_OK) {
Serial.println("Send success");
}
else {
Serial.println("Send Fail");
}
delay(1801);
}
private:
void setStatus(const char\* text) {
snprintf(\_statusText, \_statusCapacity, "%s", text);
\_status.setText(\_statusText);
//Serial.printf("from setStatus-char: %s\\n", sendData.a);
}
void addPoint(int16_t x, int16_t y, bool startsStroke) {
// Avoid filling the fixed buffer with multiple identical samples.
if (\_count && !startsStroke) {
const PaintPoint& previous = \_points\[\_count - 1\];
const int32_t dx = static_cast<int32_t>(x) - previous.x;
const int32_t dy = static_cast<int32_t>(y) - previous.y;
if (dx \* dx + dy \* dy < 9) return;
}
bool compacted = false;
if (\_count == kMaxPaintPoints) {
const size_t keep = kMaxPaintPoints / 2;
memmove(\_points, \_points + (kMaxPaintPoints - keep),
keep \* sizeof(PaintPoint));
\_count = keep;
\_points\[0\].startsStroke = true;
compacted = true;
}
PaintPoint& point = \_points\[\_count++\]; // ############# point.x point.y
point.x = x;
point.y = y;
//sprintf(bufr, "x-point: (%d), y-point: (%d) ", x, y); // My Diagnostics
//Serial.println(bufr);
point.color = kPalette\[\_colorIndex\];
point.startsStroke = startsStroke;
snprintf(\_statusText, \_statusCapacity, "TOUCH %3d, %3d", x, y); // ################ prints location
\_status.setText(\_statusText);
if (compacted) {
invalidate(bounds());
} else { // ### UiRect area?
UiRect dirty;
dirty.x = x - 5;
dirty.y = y - 5;
dirty.w = 11;
dirty.h = 11;
//sprintf(bufr, "x-dirty: (%d), y-dirty: (%d) ", dirty.x,dirty.y); //my Diagnostic
//Serial.println(bufr);
if (\_count > 1 && !startsStroke) {
const PaintPoint& previous = \_points\[\_count - 2\];
UiRect previousArea = {static_cast<int16_t>(previous.x - 5),
static_cast<int16_t>(previous.y - 5), 11, 11};
dirty = dirty.united(previousArea);
}
invalidate(dirty.intersection(bounds()));
}
}
UiLabel& \_status;
char\* \_statusText;
size_t \_statusCapacity;
PaintPoint \_points\[kMaxPaintPoints\] = {};
size_t \_count = 0;
size_t \_colorIndex = 0;
bool \_drawing = false;
};
class TouchPaintScreen : public UiScreen {
public:
TouchPaintScreen()
: \_status(\_statusText, 1),
\_paintPad(\_status, \_statusText, sizeof(\_statusText)),
\_colorButton("COLOR", {handleCommand, this, CommandColor}, 20),
\_undoButton("UNDO", {handleCommand, this, CommandUndo}, 21),
\_clearButton("CLEAR", {handleCommand, this, CommandClear}, 22),
\_B12Button("12", {handleCommand, this, CommandB12}, 23),
\_B09Button("09", {handleCommand, this, CommandB09}, 24), //#@! Button
\_B08Button("08", {handleCommand, this, CommandB08}, 25),
\_B04Button("04", {handleCommand, this, CommandB04}, 26),
\_B03Button("03", {handleCommand, this, CommandB03}, 27)
{
\_root.setBackground(kBackground);
}
// GC9A01 240x240 display
UiStatus onCreate() override {
\_status.setBounds({70, 65, 120, 16}); // x-val, y-val, width, height Original: {72, 14, 120, 16} print status on screen here
\_status.setColor(0xA69F);
\_paintPad.setBounds({73, 85, 100, 85}); // x, y; width, heitht ::smaller rectanlge Original ({38, 36, 164, 126});
\_colorButton.setBounds({18, 174, 64, 40});
\_undoButton.setBounds({88, 174, 64, 40});
\_clearButton.setBounds({158, 174, 64, 40});
\_B12Button.setBounds({90, 1, 64, 40}); //x,y loc; width & height
\_B09Button.setBounds({1,60, 64, 40}); //x,y loc; width & height
\_B08Button.setBounds({1,110, 64, 40}); //x,y loc; width & height
\_B04Button.setBounds({176,110, 64, 40}); //x,y loc; width & height
\_B03Button.setBounds({176,60, 64, 40}); //x,y loc; width & height
\_root.addChild(\_status);
\_root.addChild(\_paintPad);
\_root.addChild(\_colorButton);
\_root.addChild(\_undoButton);
\_root.addChild(\_clearButton);
\_root.addChild(\_B12Button); // Need this also
\_root.addChild(\_B09Button); // Need this also
\_root.addChild(\_B08Button); // Need this also
\_root.addChild(\_B04Button); // Need this also; // #@! Button
\_root.addChild(\_B03Button); //
return UiStatus::Ok;
}
UiWidget& root() override { return \_root; }
private:
static void handleCommand(void\* context, uint16_t commandId) {
TouchPaintScreen\* screen = static_cast<TouchPaintScreen\*>(context); // # Touch location "commands"
if (commandId == CommandColor) screen->\_paintPad.cycleColor();
else if (commandId == CommandUndo) screen->\_paintPad.undo();
else if (commandId == CommandClear) screen->\_paintPad.clear();
else if (commandId == CommandB03) screen->\_paintPad.B03(); //
else if (commandId == CommandB04) screen->\_paintPad.B04(); //#@! Button
else if (commandId == CommandB08) screen->\_paintPad.B08();
else if (commandId == CommandB09) screen->\_paintPad.B09();
else if (commandId == CommandB12) screen->\_paintPad.B12();
}
UiContainer \_root;
char \_statusText\[32\] = "Top";
UiLabel \_status;
PaintPad \_paintPad;
UiButton \_colorButton;
UiButton \_undoButton;
UiButton \_clearButton;
UiButton \_B12Button; // NOTE need this for each widget
UiButton \_B09Button; // NOTE need this for each widget
UiButton \_B08Button; // NOTE need this for each widget #@! Button
UiButton \_B04Button; // NOTE need this for each widget
UiButton \_B03Button; // NOTE need this for each widget
};
//Seeed_GFX display(Seeed_Product::Seeed_Round_Display_XIAO); //Not needed for this script?
// PaintPad already filters samples closer than 3 px; match that threshold here
// so drag strokes stay continuous without forwarding duplicate touch noise.
TouchInput touch(display, 2, 600, 3); //Threshhold
UiInputHub input;
UiTheme theme;
UiApplication application(display, input, theme);
TouchPaintScreen mainScreen;
bool uiReady = false;
void showFatalError(const char* message) {
display.fillScreen(TFT_RED);
display.setTextDatum(MC_DATUM);
display.setTextColor(TFT_WHITE);
display.setTextSize(1);
display.drawString(message ? message : "INITIALIZATION ERROR", 120, 120);
}
void printTouchBusDiagnostic() {
uint8_t frame\[5\] = {0};
const size_t received = Wire.requestFrom(static_cast<uint8_t>(0x2E),
static_cast<uint8_t>(sizeof(frame)));
size_t consumed = 0;
while (Wire.available() && consumed < sizeof(frame)) {
frame\[consumed++\] = static_cast<uint8_t>(Wire.read());
}
Serial.printf("Touch probe: I2C=%u/5 bytes, IRQ(D7)=%s, status=0x%02X\\n",
static_cast<unsigned>(received),
digitalRead(D7) == LOW ? "LOW" : "HIGH",
static_cast<unsigned>(frame\[0\]));
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
//Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing");
return;
}
//Register callback on data sent
//esp_now_register_send_cb(OnDataSent); // board version 2; NOT WORK
//Change esp_now_register_send_cb(OnDataSent); to
esp_now_register_send_cb((esp_now_send_cb_t)OnDataSent); //brd ver 3 - This works!! 101426
peerInfo.channel = 0;
peerInfo.encrypt = false;
//Register MAC1 Device
memcpy(peerInfo.peer_addr, MAC1, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer");
return;
}
//Register callback on data received
esp_now_register_recv_cb(OnDataRecv);
// ******* GFX2 init
if (!display.begin()) {
Serial.println(display.lastResult().message);
return;
}
display.setRotation(0);
Serial.println("Round Display initialized: CHSC6X uses SDA=D4, SCL=D5, IRQ=D7");
printTouchBusDiagnostic();
theme.colors.background = kBackground;
theme.colors.surface = kSurface;
theme.colors.textPrimary = TFT_WHITE;
theme.colors.textSecondary = 0x7BEF;
theme.colors.accent = 0x0451;
theme.colors.focus = kBorder;
theme.colors.disabled = 0x3186;
theme.bodyTextSize = 1;
theme.smallTextSize = 1;
theme.metrics.cornerRadius = 8;
theme.metrics.touchSlop = 5;
theme.metrics.minTouchTarget = 40;
if (!uiOk(input.add(touch))) {
showFatalError("INPUT REGISTRATION FAILED");
return;
}
const UiStatus status = application.begin(mainScreen);
if (!uiOk(status)) {
Serial.printf("UI init failed: %u\\n", static_cast<unsigned>(status));
showFatalError("UI INITIALIZATION FAILED");
return;
}
uiReady = true;
Serial.println("Touch_UI ready - touch the drawing pad or a button");
}
void loop() {
if (!uiReady) {
delay(100);
return;
}
const UiStatus status = application.tick(millis());
if (!uiOk(status)) {
Serial.printf("UI tick error: %u\\n", static_cast<unsigned>(status));
delay(100);
}
delay(10);
}