mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
phone touchscreen dialpad now available, initial iteration for alterative to keyboard number text entry; contacts export from Contacts screen to save to sd card
This commit is contained in:
@@ -77,6 +77,12 @@
|
||||
static bool smsMode = false;
|
||||
#endif
|
||||
|
||||
// Touch input (for phone dialer numpad)
|
||||
#ifdef HAS_TOUCHSCREEN
|
||||
#include "TouchInput.h"
|
||||
TouchInput touchInput(&Wire);
|
||||
#endif
|
||||
|
||||
// Power management
|
||||
#if HAS_GPS
|
||||
GPSDutyCycle gpsDuty;
|
||||
@@ -187,6 +193,135 @@
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
return restored;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// On-demand export: save current contacts to SD card.
|
||||
// Writes binary backup + human-readable listing.
|
||||
// Returns number of contacts exported, or -1 on error.
|
||||
// -----------------------------------------------------------------------
|
||||
int exportContactsToSD() {
|
||||
if (!sdCardReady) return -1;
|
||||
|
||||
// Ensure in-memory contacts are flushed to SPIFFS first
|
||||
the_mesh.saveContacts();
|
||||
|
||||
if (!SD.exists("/meshcore")) SD.mkdir("/meshcore");
|
||||
|
||||
// 1) Binary backup: SPIFFS /contacts3 → SD /meshcore/contacts.bin
|
||||
if (!SPIFFS.exists("/contacts3")) return -1;
|
||||
if (!copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin")) return -1;
|
||||
|
||||
// 2) Human-readable listing for inspection on a computer
|
||||
int count = 0;
|
||||
File txt = SD.open("/meshcore/contacts_export.txt", "w", true);
|
||||
if (txt) {
|
||||
txt.printf("Meck Contacts Export (%d total)\n", (int)the_mesh.getNumContacts());
|
||||
txt.printf("========================================\n");
|
||||
txt.printf("%-5s %-30s %s\n", "Type", "Name", "PubKey (prefix)");
|
||||
txt.printf("----------------------------------------\n");
|
||||
|
||||
ContactInfo c;
|
||||
for (uint32_t i = 0; i < (uint32_t)the_mesh.getNumContacts(); i++) {
|
||||
if (the_mesh.getContactByIdx(i, c)) {
|
||||
const char* typeStr = "???";
|
||||
switch (c.type) {
|
||||
case ADV_TYPE_CHAT: typeStr = "Chat"; break;
|
||||
case ADV_TYPE_REPEATER: typeStr = "Rptr"; break;
|
||||
case ADV_TYPE_ROOM: typeStr = "Room"; break;
|
||||
}
|
||||
// First 8 bytes of pub key as hex identifier
|
||||
char hexBuf[20];
|
||||
mesh::Utils::toHex(hexBuf, c.id.pub_key, 8);
|
||||
txt.printf("%-5s %-30s %s\n", typeStr, c.name, hexBuf);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
txt.printf("========================================\n");
|
||||
txt.printf("Total: %d contacts\n", count);
|
||||
txt.close();
|
||||
}
|
||||
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
Serial.printf("Contacts exported to SD: %d contacts\n", count);
|
||||
return count;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// On-demand import: merge contacts from SD backup into live table.
|
||||
//
|
||||
// Reads /meshcore/contacts.bin from SD and for each contact:
|
||||
// - If already in memory (matching pub_key) → skip (keep current)
|
||||
// - If NOT in memory → addContact (append to table)
|
||||
//
|
||||
// This is a non-destructive merge: you never lose contacts already in
|
||||
// memory, and you gain any that were only in the backup.
|
||||
//
|
||||
// After merging, saves the combined set back to SPIFFS so it persists.
|
||||
// Returns number of NEW contacts added, or -1 on error.
|
||||
// -----------------------------------------------------------------------
|
||||
int importContactsFromSD() {
|
||||
if (!sdCardReady) return -1;
|
||||
if (!SD.exists("/meshcore/contacts.bin")) return -1;
|
||||
|
||||
File file = SD.open("/meshcore/contacts.bin", "r");
|
||||
if (!file) return -1;
|
||||
|
||||
int added = 0;
|
||||
int skipped = 0;
|
||||
|
||||
while (true) {
|
||||
ContactInfo c;
|
||||
uint8_t pub_key[32];
|
||||
uint8_t unused;
|
||||
|
||||
// Parse one contact record (same binary format as DataStore::loadContacts)
|
||||
bool success = (file.read(pub_key, 32) == 32);
|
||||
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
|
||||
success = success && (file.read(&c.type, 1) == 1);
|
||||
success = success && (file.read(&c.flags, 1) == 1);
|
||||
success = success && (file.read(&unused, 1) == 1);
|
||||
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4);
|
||||
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
||||
success = success && (file.read(c.out_path, 64) == 64);
|
||||
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
|
||||
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
|
||||
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);
|
||||
|
||||
if (!success) break; // EOF or read error
|
||||
|
||||
c.id = mesh::Identity(pub_key);
|
||||
c.shared_secret_valid = false;
|
||||
|
||||
// Check if this contact already exists in the live table
|
||||
if (the_mesh.lookupContactByPubKey(pub_key, PUB_KEY_SIZE) != NULL) {
|
||||
skipped++;
|
||||
continue; // Already have this contact, skip
|
||||
}
|
||||
|
||||
// New contact — add to the live table
|
||||
if (the_mesh.addContact(c)) {
|
||||
added++;
|
||||
} else {
|
||||
// Table is full, stop importing
|
||||
Serial.printf("Import: table full after adding %d contacts\n", added);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
// Persist the merged set to SPIFFS
|
||||
if (added > 0) {
|
||||
the_mesh.saveContacts();
|
||||
}
|
||||
|
||||
Serial.printf("Contacts import: %d added, %d already present, %d total\n",
|
||||
added, skipped, (int)the_mesh.getNumContacts());
|
||||
return added;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Believe it or not, this std C function is busted on some platforms!
|
||||
@@ -548,6 +683,15 @@ void setup() {
|
||||
initKeyboard();
|
||||
#endif
|
||||
|
||||
// Initialize touch input (CST328)
|
||||
#ifdef HAS_TOUCHSCREEN
|
||||
if (touchInput.begin(CST328_PIN_INT)) {
|
||||
MESH_DEBUG_PRINTLN("setup() - Touch input initialized");
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("setup() - Touch input FAILED");
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SD card is already initialized (early init above).
|
||||
// Now set up SD-dependent features: message history + text reader.
|
||||
@@ -861,6 +1005,43 @@ void loop() {
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
handleKeyboardInput();
|
||||
#endif
|
||||
|
||||
// Poll touch input for phone dialer numpad
|
||||
// Hybrid debounce: finger-up detection + 150ms minimum between accepted taps.
|
||||
// The CST328 INT pin is pulse-based (not level), so getPoint() can return
|
||||
// false intermittently during a hold. Time guard prevents that from
|
||||
// causing repeat fires.
|
||||
#if defined(HAS_TOUCHSCREEN) && defined(HAS_4G_MODEM)
|
||||
{
|
||||
static bool touchFingerDown = false;
|
||||
static unsigned long lastTouchAccepted = 0;
|
||||
|
||||
if (smsMode) {
|
||||
SMSScreen* smsScr = (SMSScreen*)ui_task.getSMSScreen();
|
||||
if (smsScr && smsScr->getSubView() == SMSScreen::PHONE_DIALER) {
|
||||
int16_t tx, ty;
|
||||
if (touchInput.getPoint(tx, ty)) {
|
||||
unsigned long now = millis();
|
||||
if (!touchFingerDown && (now - lastTouchAccepted >= 150)) {
|
||||
touchFingerDown = true;
|
||||
lastTouchAccepted = now;
|
||||
if (smsScr->handleTouch(tx, ty)) {
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Only allow finger-up after 100ms from last acceptance
|
||||
// (prevents INT pulse misses from resetting state mid-hold)
|
||||
if (touchFingerDown && (millis() - lastTouchAccepted >= 100)) {
|
||||
touchFingerDown = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
touchFingerDown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1321,13 +1502,20 @@ void handleKeyboardInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Q from inbox → go home; Q from inner views is handled by SMSScreen
|
||||
// Q from app menu → go home; Q from inner views is handled by SMSScreen
|
||||
if ((key == 'q' || key == '\b') && smsScr->getSubView() == SMSScreen::APP_MENU) {
|
||||
Serial.println("Nav: SMS -> Home");
|
||||
ui_task.gotoHomeScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Phone dialer: route keys directly (letter keys map to numbers)
|
||||
if (smsScr->getSubView() == SMSScreen::PHONE_DIALER) {
|
||||
smsScr->handleInput(key);
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (smsScr->isComposing()) {
|
||||
// Composing/text input: route directly to screen, bypass injectKey()
|
||||
// to avoid UITask scheduling its own competing refresh
|
||||
@@ -1594,6 +1782,42 @@ void handleKeyboardInput() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'x':
|
||||
// Export contacts to SD card (contacts screen only)
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
Serial.println("Contacts: Exporting to SD...");
|
||||
int exported = exportContactsToSD();
|
||||
if (exported >= 0) {
|
||||
char alertBuf[48];
|
||||
snprintf(alertBuf, sizeof(alertBuf), "Exported %d to SD", exported);
|
||||
ui_task.showAlert(alertBuf, 2000);
|
||||
} else {
|
||||
ui_task.showAlert("Export failed (no SD?)", 2000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
// Import/merge contacts from SD backup (contacts screen only)
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
Serial.println("Contacts: Importing from SD...");
|
||||
int added = importContactsFromSD();
|
||||
if (added > 0) {
|
||||
// Invalidate the contacts screen cache so it rebuilds
|
||||
ContactsScreen* cs2 = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
if (cs2) cs2->invalidateCache();
|
||||
char alertBuf[48];
|
||||
snprintf(alertBuf, sizeof(alertBuf), "+%d imported (%d total)",
|
||||
added, (int)the_mesh.getNumContacts());
|
||||
ui_task.showAlert(alertBuf, 2500);
|
||||
} else if (added == 0) {
|
||||
ui_task.showAlert("No new contacts to add", 2000);
|
||||
} else {
|
||||
ui_task.showAlert("Import failed (no backup?)", 2000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
case '\b':
|
||||
// If channel screen path overlay is showing, dismiss it instead of going home
|
||||
|
||||
@@ -88,7 +88,7 @@ private:
|
||||
}
|
||||
}
|
||||
// Sort by last_advert_timestamp descending (most recently seen first)
|
||||
// Simple insertion sort — fine for up to 400 entries on ESP32
|
||||
// Simple insertion sort — fine for up to 400 entries on ESP32
|
||||
for (int i = 1; i < _filteredCount; i++) {
|
||||
uint16_t tmpIdx = _filteredIdx[i];
|
||||
uint32_t tmpTs = _filteredTs[i];
|
||||
@@ -286,17 +286,17 @@ public:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
// Left: Q:Back
|
||||
// Left: Q:Bk X:Exp
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Back");
|
||||
display.print("Q:Bk X:Exp");
|
||||
|
||||
// Center: A/D:Filter
|
||||
const char* mid = "A/D:Filtr";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
|
||||
// Right: W/S:Scroll
|
||||
const char* right = "W/S:Scrll";
|
||||
// Right: R:Imp W/S
|
||||
const char* right = "R:Imp W/S";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
|
||||
|
||||
@@ -313,8 +313,49 @@ public:
|
||||
return 5000;
|
||||
}
|
||||
|
||||
// ---- Phone dialer ----
|
||||
// ---- Phone dialer with touch numpad ----
|
||||
|
||||
// Numpad layout constants — computed dynamically from display dimensions
|
||||
static const int NUMPAD_ROWS = 5;
|
||||
static const int NUMPAD_COLS = 3;
|
||||
|
||||
// Button labels: [row][col]
|
||||
const char* numpadLabel(int row, int col) const {
|
||||
static const char* labels[5][3] = {
|
||||
{"1", "2", "3"},
|
||||
{"4", "5", "6"},
|
||||
{"7", "8", "9"},
|
||||
{"*", "0", "#"},
|
||||
{"+", "DEL", "CALL"}
|
||||
};
|
||||
return labels[row][col];
|
||||
}
|
||||
|
||||
// Button character values: '\b' = backspace, '\r' = call
|
||||
char numpadChar(int row, int col) const {
|
||||
static const char chars[5][3] = {
|
||||
{'1', '2', '3'},
|
||||
{'4', '5', '6'},
|
||||
{'7', '8', '9'},
|
||||
{'*', '0', '#'},
|
||||
{'+', '\b', '\r'}
|
||||
};
|
||||
return chars[row][col];
|
||||
}
|
||||
|
||||
int renderPhoneDialer(DisplayDriver& display) {
|
||||
int W = display.width();
|
||||
int H = display.height();
|
||||
|
||||
// Layout regions (dynamic based on display size)
|
||||
int headerH = 12;
|
||||
int phoneFieldH = 14;
|
||||
int footerH = 12;
|
||||
int numpadTop = headerH + phoneFieldH;
|
||||
int numpadH = H - numpadTop - footerH;
|
||||
int rowH = numpadH / NUMPAD_ROWS;
|
||||
int colW = W / NUMPAD_COLS;
|
||||
|
||||
// Header
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
@@ -322,45 +363,87 @@ public:
|
||||
display.print("Dial Number");
|
||||
|
||||
// Signal strength at top-right
|
||||
renderSignalIndicator(display, display.width() - 2, 0);
|
||||
renderSignalIndicator(display, W - 2, 0);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// Phone number input
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(4, 24);
|
||||
display.print("Enter phone number:");
|
||||
display.drawRect(0, headerH - 1, W, 1);
|
||||
|
||||
// Phone number field
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(4, 40);
|
||||
display.setCursor(2, headerH + 2);
|
||||
if (_phoneInputPos > 0) {
|
||||
display.print(_phoneInputBuf);
|
||||
}
|
||||
display.print("_");
|
||||
|
||||
// Hint if empty
|
||||
if (_phoneInputPos == 0) {
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.setCursor(4, 58);
|
||||
display.print("digits, +, *, #");
|
||||
display.setTextSize(1);
|
||||
// Separator above numpad
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, numpadTop - 1, W, 1);
|
||||
|
||||
// Draw numpad grid
|
||||
for (int row = 0; row < NUMPAD_ROWS; row++) {
|
||||
int y = numpadTop + row * rowH;
|
||||
|
||||
// Row separator
|
||||
if (row > 0) {
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.drawRect(0, y, W, 1);
|
||||
}
|
||||
|
||||
for (int col = 0; col < NUMPAD_COLS; col++) {
|
||||
int x = col * colW;
|
||||
|
||||
// Column separator
|
||||
if (col > 0) {
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.drawRect(x, y, 1, rowH);
|
||||
}
|
||||
|
||||
// Button label - centered in cell
|
||||
const char* label = numpadLabel(row, col);
|
||||
bool isAction = (row == 4); // Bottom row has action buttons
|
||||
|
||||
if (isAction) {
|
||||
display.setTextSize(0);
|
||||
if (col == 2 && _phoneInputPos > 0) {
|
||||
display.setColor(DisplayDriver::GREEN); // CALL
|
||||
} else if (col == 1) {
|
||||
display.setColor(DisplayDriver::YELLOW); // DEL
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
} else {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
uint16_t textW = display.getTextWidth(label);
|
||||
int textH = isAction ? 7 : 8;
|
||||
int cx = x + (colW - textW) / 2;
|
||||
int cy = y + (rowH - textH) / 2;
|
||||
display.setCursor(cx, cy);
|
||||
display.print(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
int footerY = H - footerH;
|
||||
display.drawRect(0, footerY - 1, W, 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Back");
|
||||
display.print("Q:Bk");
|
||||
if (_phoneInputPos > 0) {
|
||||
const char* rt = "Ent:Call";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.setCursor(W - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
} else {
|
||||
// Hint: letter keys type numbers directly
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
const char* hint = "W-C=1-9";
|
||||
display.setCursor(W - display.getTextWidth(hint) - 2, footerY);
|
||||
display.print(hint);
|
||||
}
|
||||
|
||||
return 2000;
|
||||
@@ -820,7 +903,28 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Phone dialer input ----
|
||||
// ---- Phone dialer input (keyboard) ----
|
||||
//
|
||||
// Three ways to enter digits:
|
||||
// 1. Touch the on-screen numpad
|
||||
// 2. Sym+key (normal keyboard number entry)
|
||||
// 3. Just press the letter key — the dialer maps it automatically
|
||||
// using the silk-screened number labels on the keyboard:
|
||||
// w=1 e=2 r=3 | s=4 d=5 f=6 | z=7 x=8 c=9
|
||||
// q=# a=* o=+ | 0=mic key (arrives as sym+'0')
|
||||
|
||||
// Map a letter key to its dialer equivalent (0 = no mapping)
|
||||
// Note: 'q' is reserved for back navigation, use sym+q or touch for '#'
|
||||
char dialerKeyMap(char c) {
|
||||
switch (c) {
|
||||
case 'w': return '1'; case 'e': return '2'; case 'r': return '3';
|
||||
case 's': return '4'; case 'd': return '5'; case 'f': return '6';
|
||||
case 'z': return '7'; case 'x': return '8'; case 'c': return '9';
|
||||
case 'a': return '*'; case 'o': return '+';
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool handlePhoneDialerInput(char c) {
|
||||
switch (c) {
|
||||
case '\r': // Enter - place call
|
||||
@@ -838,23 +942,105 @@ public:
|
||||
}
|
||||
return true;
|
||||
|
||||
case 'q': case 'Q': // Back to app menu
|
||||
case 'q': // Back to app menu
|
||||
_phoneInputBuf[0] = '\0';
|
||||
_phoneInputPos = 0;
|
||||
_view = APP_MENU;
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Accept phone number characters
|
||||
if (_phoneInputPos < SMS_PHONE_LEN - 1 &&
|
||||
((c >= '0' && c <= '9') || c == '+' || c == '*' || c == '#')) {
|
||||
_phoneInputBuf[_phoneInputPos++] = c;
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
// Accept phone number characters directly (from sym+key)
|
||||
if ((c >= '0' && c <= '9') || c == '+' || c == '*' || c == '#') {
|
||||
if (_phoneInputPos < SMS_PHONE_LEN - 1) {
|
||||
_phoneInputBuf[_phoneInputPos++] = c;
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Map plain letter keys to their silk-screened number equivalents
|
||||
char mapped = dialerKeyMap(c);
|
||||
if (mapped) {
|
||||
if (_phoneInputPos < SMS_PHONE_LEN - 1) {
|
||||
_phoneInputBuf[_phoneInputPos++] = mapped;
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Touch numpad input (called from main loop when touch detected) ----
|
||||
|
||||
// Process a touch event at PHYSICAL display coordinates (px, py).
|
||||
// The touch controller reports 240x320; display draws in virtual coords.
|
||||
// Returns true if the touch was consumed (a button was pressed).
|
||||
// Caller should call forceRefresh() after this returns true.
|
||||
//
|
||||
// dispW/dispH: virtual display dimensions (display.width()/height())
|
||||
// physW/physH: physical touch panel dimensions (240/320)
|
||||
bool handleTouch(int16_t px, int16_t py, int dispW = 128, int dispH = 128,
|
||||
int physW = 240, int physH = 320) {
|
||||
if (_view != PHONE_DIALER) return false;
|
||||
|
||||
// Map physical touch coordinates to virtual display coordinates
|
||||
int x = (int)px * dispW / physW;
|
||||
int y = (int)py * dispH / physH;
|
||||
|
||||
// Compute layout (must match renderPhoneDialer)
|
||||
int headerH = 12;
|
||||
int phoneFieldH = 14;
|
||||
int footerH = 12;
|
||||
int numpadTop = headerH + phoneFieldH;
|
||||
int numpadH = dispH - numpadTop - footerH;
|
||||
int rowH = numpadH / NUMPAD_ROWS;
|
||||
int colW = dispW / NUMPAD_COLS;
|
||||
|
||||
// Check bounds: must be within numpad grid
|
||||
if (y < numpadTop || y >= numpadTop + NUMPAD_ROWS * rowH) return false;
|
||||
if (x < 0 || x >= NUMPAD_COLS * colW) return false;
|
||||
|
||||
// Map coordinates to grid cell
|
||||
int col = x / colW;
|
||||
int row = (y - numpadTop) / rowH;
|
||||
|
||||
if (col < 0 || col >= NUMPAD_COLS || row < 0 || row >= NUMPAD_ROWS) return false;
|
||||
|
||||
char c = numpadChar(row, col);
|
||||
bool changed = false;
|
||||
|
||||
if (c == '\r') {
|
||||
// CALL button
|
||||
if (_phoneInputPos > 0) {
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
modemManager.dialCall(_phoneInputBuf);
|
||||
changed = true;
|
||||
}
|
||||
} else if (c == '\b') {
|
||||
// DEL button
|
||||
if (_phoneInputPos > 0) {
|
||||
_phoneInputPos--;
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
// Digit/symbol button
|
||||
if (_phoneInputPos < SMS_PHONE_LEN - 1) {
|
||||
_phoneInputBuf[_phoneInputPos++] = c;
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[Touch] Numpad: phys=(%d,%d) virt=(%d,%d) row=%d col=%d btn=%s %s\n",
|
||||
px, py, x, y, row, col, numpadLabel(row, col),
|
||||
changed ? "OK" : "NOOP");
|
||||
return changed;
|
||||
}
|
||||
|
||||
// clearTouch() is a no-op with time-based debounce, kept for API compat
|
||||
void clearTouch() { }
|
||||
|
||||
// ---- Inbox input ----
|
||||
bool handleInboxInput(char c) {
|
||||
switch (c) {
|
||||
|
||||
128
examples/companion_radio/ui-new/Touchinput.h
Normal file
128
examples/companion_radio/ui-new/Touchinput.h
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// TouchInput - Minimal CST328/CST3530 touch driver for T-Deck Pro
|
||||
//
|
||||
// Uses raw I2C reads on the shared Wire bus. No external library needed.
|
||||
// Protocol confirmed via raw serial capture from actual hardware:
|
||||
//
|
||||
// Register 0xD000, 7 bytes:
|
||||
// buf[0]: event flags (0xAB = idle/no touch, other = active touch)
|
||||
// buf[1]: X coordinate high data
|
||||
// buf[2]: Y coordinate high data
|
||||
// buf[3]: X low nibble (bits 7:4) | Y low nibble (bits 3:0)
|
||||
// buf[4]: pressure
|
||||
// buf[5]: touch count (& 0x7F), typically 0x01 for single touch
|
||||
// buf[6]: 0xAB always (check byte, ignore)
|
||||
//
|
||||
// Coordinate formula:
|
||||
// x = (buf[1] << 4) | ((buf[3] >> 4) & 0x0F) → 0..239
|
||||
// y = (buf[2] << 4) | (buf[3] & 0x0F) → 0..319
|
||||
//
|
||||
// Hardware: CST328 at 0x1A, INT=GPIO12, RST=GPIO38 (V1.1)
|
||||
//
|
||||
// Guard: HAS_TOUCHSCREEN
|
||||
// =============================================================================
|
||||
|
||||
#ifdef HAS_TOUCHSCREEN
|
||||
|
||||
#ifndef TOUCH_INPUT_H
|
||||
#define TOUCH_INPUT_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
class TouchInput {
|
||||
public:
|
||||
static const uint8_t TOUCH_ADDR = 0x1A;
|
||||
|
||||
TouchInput(TwoWire* wire = &Wire)
|
||||
: _wire(wire), _intPin(-1), _initialized(false), _debugCount(0), _lastPoll(0) {}
|
||||
|
||||
bool begin(int intPin) {
|
||||
_intPin = intPin;
|
||||
pinMode(_intPin, INPUT);
|
||||
|
||||
// Verify the touch controller is present on the bus
|
||||
_wire->beginTransmission(TOUCH_ADDR);
|
||||
uint8_t err = _wire->endTransmission();
|
||||
if (err != 0) {
|
||||
Serial.printf("[Touch] CST328 not found at 0x%02X (err=%d)\n", TOUCH_ADDR, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[Touch] CST328 found at 0x%02X, INT=GPIO%d\n", TOUCH_ADDR, _intPin);
|
||||
_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isReady() const { return _initialized; }
|
||||
|
||||
// Poll for touch. Returns true if a finger is down, fills x and y.
|
||||
// Coordinates are in physical display space (0-239 X, 0-319 Y).
|
||||
// NOTE: CST328 INT pin is pulse-based, not level. We cannot rely on
|
||||
// digitalRead(INT) for touch state. Instead, always read and check buf[0].
|
||||
bool getPoint(int16_t &x, int16_t &y) {
|
||||
if (!_initialized) return false;
|
||||
|
||||
// Rate limit: poll at most every 20ms (50 Hz) to avoid I2C bus congestion
|
||||
unsigned long now = millis();
|
||||
if (now - _lastPoll < 20) return false;
|
||||
_lastPoll = now;
|
||||
|
||||
uint8_t buf[7];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
|
||||
// Write register address 0xD000
|
||||
_wire->beginTransmission(TOUCH_ADDR);
|
||||
_wire->write(0xD0);
|
||||
_wire->write(0x00);
|
||||
if (_wire->endTransmission(false) != 0) return false;
|
||||
|
||||
// Read 7 bytes of touch data
|
||||
uint8_t received = _wire->requestFrom(TOUCH_ADDR, (uint8_t)7);
|
||||
if (received < 7) return false;
|
||||
for (int i = 0; i < 7; i++) buf[i] = _wire->read();
|
||||
|
||||
// buf[0] == 0xAB means idle (no touch active)
|
||||
if (buf[0] == 0xAB) return false;
|
||||
|
||||
// buf[0] == 0x00 can appear on finger-up transition — ignore
|
||||
if (buf[0] == 0x00) return false;
|
||||
|
||||
// Touch count from buf[5]
|
||||
uint8_t count = buf[5] & 0x7F;
|
||||
if (count == 0 || count > 5) return false;
|
||||
|
||||
// Parse coordinates (CST226/CST328 format confirmed by hardware capture)
|
||||
// x = (buf[1] << 4) | high nibble of buf[3]
|
||||
// y = (buf[2] << 4) | low nibble of buf[3]
|
||||
int16_t tx = ((int16_t)buf[1] << 4) | ((buf[3] >> 4) & 0x0F);
|
||||
int16_t ty = ((int16_t)buf[2] << 4) | (buf[3] & 0x0F);
|
||||
|
||||
// Sanity check (panel is 240x320)
|
||||
if (tx < 0 || tx > 260 || ty < 0 || ty > 340) return false;
|
||||
|
||||
// Debug: log first 20 touch events with parsed coordinates
|
||||
if (_debugCount < 50) {
|
||||
Serial.printf("[Touch] Raw: %02X %02X %02X %02X %02X %02X %02X → x=%d y=%d\n",
|
||||
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6],
|
||||
tx, ty);
|
||||
_debugCount++;
|
||||
}
|
||||
|
||||
x = tx;
|
||||
y = ty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
TwoWire* _wire;
|
||||
int _intPin;
|
||||
bool _initialized;
|
||||
int _debugCount;
|
||||
unsigned long _lastPoll;
|
||||
};
|
||||
|
||||
#endif // TOUCH_INPUT_H
|
||||
#endif // HAS_TOUCHSCREEN
|
||||
Reference in New Issue
Block a user