From 4cc15f7ab0f40b4bbd36825817c2d36f7d98b684 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sat, 23 May 2026 04:04:45 +1000 Subject: [PATCH] Delete t-echo card and t-echo lite folders as no longer working on those; support json import & export config; new method for creating private and hashtag channels --- examples/companion_radio/MyMesh.cpp | 29 + examples/companion_radio/MyMesh.h | 57 +- examples/companion_radio/main.cpp | 31 + .../companion_radio/ui-new/Settingsscreen.h | 243 ++++- variants/lilygo_techo_card/Cpupowermanager.h | 16 - variants/lilygo_techo_card/GPSStreamCounter.h | 76 -- variants/lilygo_techo_card/Radiopresets.h | 34 - variants/lilygo_techo_card/TechoCardBoard.cpp | 339 ------- variants/lilygo_techo_card/TechoCardBoard.h | 100 -- .../lilygo_techo_card/TechoCardHomeScreen.h | 888 ------------------ variants/lilygo_techo_card/pins_arduino.h | 68 -- variants/lilygo_techo_card/platformio.ini | 154 --- variants/lilygo_techo_card/sd.h | 44 - variants/lilygo_techo_card/target.cpp | 58 -- variants/lilygo_techo_card/target.h | 45 - variants/lilygo_techo_card/variant.cpp | 11 - variants/lilygo_techo_card/variant.h | 203 ---- .../lilygo_techo_lite_WIP/CPUPowerManager.h | 15 - variants/lilygo_techo_lite_WIP/FS.h | 19 - variants/lilygo_techo_lite_WIP/SD.h | 43 - variants/lilygo_techo_lite_WIP/TechoBoard.cpp | 54 -- variants/lilygo_techo_lite_WIP/TechoBoard.h | 43 - variants/lilygo_techo_lite_WIP/platformio.ini | 268 ------ variants/lilygo_techo_lite_WIP/target.cpp | 55 -- variants/lilygo_techo_lite_WIP/target.h | 32 - variants/lilygo_techo_lite_WIP/variant.cpp | 39 - variants/lilygo_techo_lite_WIP/variant.h | 159 ---- 27 files changed, 335 insertions(+), 2788 deletions(-) delete mode 100644 variants/lilygo_techo_card/Cpupowermanager.h delete mode 100644 variants/lilygo_techo_card/GPSStreamCounter.h delete mode 100644 variants/lilygo_techo_card/Radiopresets.h delete mode 100644 variants/lilygo_techo_card/TechoCardBoard.cpp delete mode 100644 variants/lilygo_techo_card/TechoCardBoard.h delete mode 100644 variants/lilygo_techo_card/TechoCardHomeScreen.h delete mode 100644 variants/lilygo_techo_card/pins_arduino.h delete mode 100644 variants/lilygo_techo_card/platformio.ini delete mode 100644 variants/lilygo_techo_card/sd.h delete mode 100644 variants/lilygo_techo_card/target.cpp delete mode 100644 variants/lilygo_techo_card/target.h delete mode 100644 variants/lilygo_techo_card/variant.cpp delete mode 100644 variants/lilygo_techo_card/variant.h delete mode 100644 variants/lilygo_techo_lite_WIP/CPUPowerManager.h delete mode 100644 variants/lilygo_techo_lite_WIP/FS.h delete mode 100644 variants/lilygo_techo_lite_WIP/SD.h delete mode 100644 variants/lilygo_techo_lite_WIP/TechoBoard.cpp delete mode 100644 variants/lilygo_techo_lite_WIP/TechoBoard.h delete mode 100644 variants/lilygo_techo_lite_WIP/platformio.ini delete mode 100644 variants/lilygo_techo_lite_WIP/target.cpp delete mode 100644 variants/lilygo_techo_lite_WIP/target.h delete mode 100644 variants/lilygo_techo_lite_WIP/variant.cpp delete mode 100644 variants/lilygo_techo_lite_WIP/variant.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index df5a0d77..50363bba 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -661,6 +661,34 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t _voiceEnvHandler(from.name, text); } + // Intercept channel share messages before BLE gets them + if (text && strncmp(text, MECK_CH_PREFIX, MECK_CH_PREFIX_LEN) == 0) { + const char* payload = text + MECK_CH_PREFIX_LEN; + const char* sep = strchr(payload, '|'); + if (sep && (sep - payload) > 0 && (sep - payload) < 32) { + char chName[32]; + int nameLen = sep - payload; + memcpy(chName, payload, nameLen); + chName[nameLen] = '\0'; + + // Parse hex secret (32 hex chars = 16 bytes) + const char* hexStr = sep + 1; + int hexLen = strlen(hexStr); + if (hexLen >= 32) { + uint8_t secret[16]; + mesh::Utils::fromHex(secret, 16, hexStr); + addPendingInvite(chName, secret, from.name); + Serial.printf("Channel invite from %s: '%s'\n", from.name, chName); + } + + // Sanitise display text + char sanitised[64]; + snprintf(sanitised, sizeof(sanitised), "Shared channel: %s", chName); + queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, sanitised); + return; + } + } + queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); } @@ -1327,6 +1355,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe memset(send_scope.key, 0, sizeof(send_scope.key)); memset(_sent_track, 0, sizeof(_sent_track)); _sent_track_idx = 0; + memset(_pendingInvites, 0, sizeof(_pendingInvites)); _admin_contact_idx = -1; _discoveredCount = 0; _discoveryActive = false; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 821639e3..d12f3e7c 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -96,10 +96,22 @@ struct AdvertPath { struct DiscoveredNode { ContactInfo contact; uint8_t path_len; - int8_t snr; // SNR × 4 from active discovery response (0 if pre-seeded) + int8_t snr; // SNR x 4 from active discovery response (0 if pre-seeded) bool already_in_contacts; // true if contact was auto-added or already known }; +// Channel invite received via DM -- stored in RAM until accepted/dismissed +#define MAX_PENDING_INVITES 8 +#define MECK_CH_PREFIX "[MECK:CH]" +#define MECK_CH_PREFIX_LEN 9 + +struct PendingChannelInvite { + char name[32]; // channel name + uint8_t secret[16]; // channel secret (CIPHER_KEY_SIZE bytes) + char senderName[32]; // who shared it + bool active; // is this slot in use +}; + class MyMesh : public BaseChatMesh, public DataStoreHost { public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); @@ -257,6 +269,48 @@ public: _store->saveMainIdentity(self_id); } + // --- Pending channel invites (received via DM) --- + int getPendingInviteCount() const { + int count = 0; + for (int i = 0; i < MAX_PENDING_INVITES; i++) { + if (_pendingInvites[i].active) count++; + } + return count; + } + const PendingChannelInvite* getPendingInvite(int idx) const { + int seen = 0; + for (int i = 0; i < MAX_PENDING_INVITES; i++) { + if (_pendingInvites[i].active) { + if (seen == idx) return &_pendingInvites[i]; + seen++; + } + } + return nullptr; + } + bool addPendingInvite(const char* name, const uint8_t* secret, const char* senderName) { + for (int i = 0; i < MAX_PENDING_INVITES; i++) { + if (!_pendingInvites[i].active) { + strncpy(_pendingInvites[i].name, name, 31); + _pendingInvites[i].name[31] = '\0'; + memcpy(_pendingInvites[i].secret, secret, 16); + strncpy(_pendingInvites[i].senderName, senderName, 31); + _pendingInvites[i].senderName[31] = '\0'; + _pendingInvites[i].active = true; + return true; + } + } + return false; // no free slots + } + void removePendingInvite(int idx) { + int seen = 0; + for (int i = 0; i < MAX_PENDING_INVITES; i++) { + if (_pendingInvites[i].active) { + if (seen == idx) { _pendingInvites[i].active = false; return; } + seen++; + } + } + } + private: void writeOKFrame(); void writeErrFrame(uint8_t err_code); @@ -282,6 +336,7 @@ private: mutable bool _forceNextImport = false; bool _deferSaves = false; unsigned long _lastUserInput = 0; // millis() of last keypress -- defer saves until idle + PendingChannelInvite _pendingInvites[MAX_PENDING_INVITES]; // RAM-only pending channel shares uint32_t pending_login; uint32_t pending_status; uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index fe2e04ec..6cd33679 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -4591,6 +4591,37 @@ void handleKeyboardInput() { } } #endif + // Check for channel share request from the settings screen + if (settings->isShareRequested()) { + int contactIdx = settings->getShareContactIdx(); + uint8_t channelIdx = settings->getShareChannelIdx(); + settings->clearShareRequest(); + + ChannelDetails ch; + ContactInfo contact; + if (the_mesh.getChannel(channelIdx, ch) && ch.name[0] != '\0' + && the_mesh.getContactByIdx(contactIdx, contact)) { + // Build share message: [MECK:CH]name|secret_hex + char shareMsg[128]; + char hexSecret[33]; + mesh::Utils::toHex(hexSecret, ch.channel.secret, 16); + snprintf(shareMsg, sizeof(shareMsg), "%s%s|%s", + MECK_CH_PREFIX, ch.name, hexSecret); + + if (the_mesh.uiSendDirectMessage((uint32_t)contactIdx, shareMsg)) { + // Add sanitised version to DM conversation view + char displayMsg[64]; + snprintf(displayMsg, sizeof(displayMsg), "Shared channel: %s", ch.name); + ui_task.addSentDM(contact.name, the_mesh.getNodePrefs()->node_name, displayMsg); + + char alertBuf[48]; + snprintf(alertBuf, sizeof(alertBuf), "Shared with %s", contact.name); + ui_task.showAlert(alertBuf, 2000); + } else { + ui_task.showAlert("Share failed", 1500); + } + } + } return; } diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index b5714050..784bcf83 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -164,7 +164,8 @@ enum SettingsRowType : uint8_t { ROW_CHANNELS_SUBMENU, // Folder row → enters Channels sub-screen ROW_CH_HEADER, // "--- Channels ---" separator ROW_CHANNEL, // A channel entry (dynamic, index stored separately) - ROW_ADD_CHANNEL, // "+ Add Hashtag Channel" + ROW_ADD_CHANNEL, // "+ Add Channel (# = public)" + ROW_PENDING_INVITE, // Pending channel invite (param = invite index) #ifdef HAS_SDCARD ROW_EXPORT_IMPORT_SUBMENU, // Folder row: "Export/Import >>" ROW_EXPORT_TO_SD, // "Export to SD >>" (enters flags sub-screen) @@ -201,6 +202,7 @@ enum EditMode : uint8_t { EDIT_NUMBER, // W/S adjusts value (freq, BW, SF, CR, TX, UTC) EDIT_CONFIRM, // Confirmation dialog (delete channel, apply radio) EDIT_NOTIF_SOUND, // Sound picker for per-channel notification tone + EDIT_SHARE_PICK, // Contact picker for channel sharing #ifdef MECK_WIFI_COMPANION EDIT_WIFI, // WiFi scan/select/password flow #endif @@ -307,6 +309,16 @@ private: bool _importRequested; // set by key handler, cleared by main.cpp after calling import #endif + // Channel share picker state + #define SHARE_MAX_CONTACTS 32 + uint8_t _shareChannelIdx; // channel being shared + int _shareContacts[SHARE_MAX_CONTACTS]; // indices of DM-capable contacts + int _shareContactCount; // number of entries in _shareContacts + int _sharePickerIdx; // highlighted item in picker + int _sharePickerScroll; // scroll offset in picker + bool _shareRequested; // flag for main.cpp + int _shareContactIdx; // selected contact index (for main.cpp) + // Dirty flag for radio params — prompt to apply bool _radioChanged; @@ -433,6 +445,10 @@ private: } } addRow(ROW_ADD_CHANNEL); + // Pending channel invites (received via DM) + for (int pi = 0; pi < the_mesh.getPendingInviteCount(); pi++) { + addRow(ROW_PENDING_INVITE, pi); + } #ifdef MECK_OTA_UPDATE } else if (_subScreen == SUB_OTA_TOOLS) { // --- OTA Tools sub-screen --- @@ -564,28 +580,33 @@ private: // Hashtag channel creation // --------------------------------------------------------------------------- - void createHashtagChannel(const char* name) { - // Build channel name with # prefix if not already present - char chanName[32]; - if (name[0] == '#') { - strncpy(chanName, name, sizeof(chanName)); - } else { - chanName[0] = '#'; - strncpy(&chanName[1], name, sizeof(chanName) - 1); - } - chanName[31] = '\0'; - - // Generate 128-bit PSK from SHA-256 of channel name + void createChannel(const char* name) { ChannelDetails newCh; memset(&newCh, 0, sizeof(newCh)); - strncpy(newCh.name, chanName, sizeof(newCh.name)); - newCh.name[31] = '\0'; - // SHA-256 the channel name → first 16 bytes become the secret - uint8_t hash[32]; - mesh::Utils::sha256(hash, 32, (const uint8_t*)chanName, strlen(chanName)); - memcpy(newCh.channel.secret, hash, 16); - // Upper 16 bytes left as zero → setChannel uses 128-bit mode + if (name[0] == '#') { + // Public hashtag channel -- derive secret from SHA-256 of name + strncpy(newCh.name, name, sizeof(newCh.name)); + newCh.name[31] = '\0'; + + uint8_t hash[32]; + mesh::Utils::sha256(hash, 32, (const uint8_t*)name, strlen(name)); + memcpy(newCh.channel.secret, hash, 16); + Serial.printf("Settings: Creating public channel '%s'\n", name); + } else { + // Private channel -- random 16-byte secret + strncpy(newCh.name, name, sizeof(newCh.name)); + newCh.name[31] = '\0'; + + uint8_t secret[16]; + uint32_t r; + for (int i = 0; i < 16; i++) { + if (i % 4 == 0) r = esp_random(); + secret[i] = (r >> ((i % 4) * 8)) & 0xFF; + } + memcpy(newCh.channel.secret, secret, 16); + Serial.printf("Settings: Creating private channel '%s'\n", name); + } // Find next empty slot for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { @@ -593,13 +614,14 @@ private: if (!the_mesh.getChannel(i, existing) || existing.name[0] == '\0') { if (the_mesh.setChannel(i, newCh)) { the_mesh.saveChannels(); - Serial.printf("Settings: Created hashtag channel '%s' at idx %d\n", chanName, i); + Serial.printf("Settings: Channel '%s' created at idx %d\n", newCh.name, i); } break; } } } + void deleteChannel(uint8_t idx) { // Clear the channel by writing an empty ChannelDetails // Then compact: shift all channels above it down by one @@ -656,6 +678,12 @@ public: _exportRequested = false; _importRequested = false; #endif + _shareChannelIdx = 0; + _shareContactCount = 0; + _sharePickerIdx = 0; + _sharePickerScroll = 0; + _shareRequested = false; + _shareContactIdx = -1; #ifdef MECK_OTA_UPDATE _otaServer = nullptr; _otaPhase = OTA_PHASE_CONFIRM; @@ -686,6 +714,9 @@ public: _exportRequested = false; _importRequested = false; #endif + _shareRequested = false; + _shareContactIdx = -1; + _shareContactCount = 0; #ifdef HAS_4G_MODEM _modemEnabled = ModemManager::loadEnabledConfig(); #endif @@ -885,6 +916,12 @@ public: void clearImportRequest() { _importRequested = false; } #endif + // Channel share request -- checked and cleared by main.cpp + bool isShareRequested() const { return _shareRequested; } + int getShareContactIdx() const { return _shareContactIdx; } + uint8_t getShareChannelIdx() const { return _shareChannelIdx; } + void clearShareRequest() { _shareRequested = false; _shareContactIdx = -1; } + // --------------------------------------------------------------------------- // OTA firmware update // --------------------------------------------------------------------------- @@ -2111,14 +2148,24 @@ public: case ROW_ADD_CHANNEL: if (editing && _editMode == EDIT_TEXT) { - snprintf(tmp, sizeof(tmp), "# %s_", _editBuf); + snprintf(tmp, sizeof(tmp), "> %s_", _editBuf); } else { display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN); - strcpy(tmp, "+ Add Hashtag Channel"); + strcpy(tmp, "+ Add Channel (# = public)"); } display.print(tmp); break; + case ROW_PENDING_INVITE: { + const PendingChannelInvite* inv = the_mesh.getPendingInvite(_rows[i].param); + if (inv) { + display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::YELLOW); + snprintf(tmp, sizeof(tmp), "Pending: %s", inv->name); + display.print(tmp); + } + break; + } + #ifdef MECK_OTA_UPDATE case ROW_OTA_TOOLS_SUBMENU: display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN); @@ -2568,6 +2615,69 @@ public: } #endif + // === Share contact picker overlay === + if (_editMode == EDIT_SHARE_PICK) { + int bx = 2, by = 14, bw = display.width() - 4; + int bh = display.height() - 28; + display.setColor(DisplayDriver::DARK); + display.fillRect(bx, by, bw, bh); + display.setColor(DisplayDriver::LIGHT); + display.drawRect(bx, by, bw, bh); + + display.setTextSize(_prefs->smallTextSize()); + int lineH = _prefs->smallLineH(); + + // Header + display.setColor(DisplayDriver::GREEN); + display.setCursor(bx + 4, by + 3); + display.print("Share with contact:"); + + if (_shareContactCount == 0) { + display.setColor(DisplayDriver::LIGHT); + display.setCursor(bx + 4, by + 16); + display.print("No contacts available"); + } else { + int listTop = by + 14; + int listBot = by + bh - 14; + int maxVisible = (listBot - listTop) / lineH; + if (maxVisible < 3) maxVisible = 3; + + // Scroll to keep selection visible + if (_sharePickerIdx < _sharePickerScroll) _sharePickerScroll = _sharePickerIdx; + if (_sharePickerIdx >= _sharePickerScroll + maxVisible) _sharePickerScroll = _sharePickerIdx - maxVisible + 1; + + for (int vi = 0; vi < maxVisible && (_sharePickerScroll + vi) < _shareContactCount; vi++) { + int ci = _sharePickerScroll + vi; + int iy = listTop + vi * lineH; + bool sel = (ci == _sharePickerIdx); + + if (sel) { + display.setColor(DisplayDriver::GREEN); + display.fillRect(bx + 1, iy, bw - 2, lineH); + display.setColor(DisplayDriver::DARK); + } else { + display.setColor(DisplayDriver::LIGHT); + } + + ContactInfo ci_info; + if (the_mesh.getContactByIdx(_shareContacts[ci], ci_info)) { + display.setCursor(bx + 4, iy + 1); + display.print(ci_info.name); + } + } + } + + // Footer hint + display.setColor(DisplayDriver::YELLOW); + display.setCursor(bx + 4, by + bh - 12); + #if defined(LilyGo_T5S3_EPaper_Pro) + display.print("Tap:Send Boot:Cancel"); + #else + display.print("Enter:Send Q:Cancel"); + #endif + display.setTextSize(1); + } + // === Footer === int footerY = display.height() - 12; display.drawRect(0, footerY - 2, display.width(), 1); @@ -2817,6 +2927,33 @@ public: } #endif + // --- Share contact picker --- + if (_editMode == EDIT_SHARE_PICK) { + if (c == 'w' || c == 'W' || c == 0xF2 || c == KEY_UP) { + if (_sharePickerIdx > 0) _sharePickerIdx--; + return true; + } + if (c == 's' || c == 'S' || c == 0xF1 || c == KEY_DOWN) { + if (_sharePickerIdx < _shareContactCount - 1) _sharePickerIdx++; + return true; + } + if (c == '\r' || c == 13) { + if (_shareContactCount > 0) { + _shareContactIdx = _shareContacts[_sharePickerIdx]; + _shareRequested = true; + Serial.printf("Settings: share channel %d with contact %d\n", + _shareChannelIdx, _shareContactIdx); + } + _editMode = EDIT_NONE; + return true; + } + if (c == 'q' || c == 'Q' || c == '\b') { + _editMode = EDIT_NONE; + return true; + } + return true; // consume all keys in picker mode + } + #ifdef MECK_OTA_UPDATE // --- OTA update flow --- if (_editMode == EDIT_OTA) { @@ -3015,7 +3152,7 @@ public: _editMode = EDIT_NONE; } else if (type == ROW_ADD_CHANNEL) { if (_editPos > 0) { - createHashtagChannel(_editBuf); + createChannel(_editBuf); rebuildRows(); } _editMode = EDIT_NONE; @@ -3520,6 +3657,35 @@ public: case ROW_ADD_CHANNEL: startEditText(""); break; + + case ROW_PENDING_INVITE: { + // Accept pending channel invite + const PendingChannelInvite* inv = the_mesh.getPendingInvite(_rows[_cursor].param); + if (inv) { + ChannelDetails newCh; + memset(&newCh, 0, sizeof(newCh)); + strncpy(newCh.name, inv->name, sizeof(newCh.name)); + newCh.name[31] = '\0'; + memcpy(newCh.channel.secret, inv->secret, 16); + + // Find next empty slot + bool added = false; + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails existing; + if (!the_mesh.getChannel(i, existing) || existing.name[0] == '\0') { + if (the_mesh.setChannel(i, newCh)) { + the_mesh.saveChannels(); + Serial.printf("Settings: Accepted channel '%s' at idx %d\n", inv->name, i); + added = true; + } + break; + } + } + the_mesh.removePendingInvite(_rows[_cursor].param); + rebuildRows(); + } + break; + } #ifdef MECK_OTA_UPDATE case ROW_OTA_TOOLS_SUBMENU: _savedTopCursor = _cursor; @@ -3609,12 +3775,19 @@ public: } // X: delete channel (when on a channel row, idx > 0) + // dismiss pending invite (when on a pending invite row) if (c == 'x' || c == 'X') { if (_rows[_cursor].type == ROW_CHANNEL && _rows[_cursor].param > 0) { _editMode = EDIT_CONFIRM; _confirmAction = 1; return true; } + if (_rows[_cursor].type == ROW_PENDING_INVITE) { + the_mesh.removePendingInvite(_rows[_cursor].param); + rebuildRows(); + Serial.println("Settings: dismissed pending channel invite"); + return true; + } } // N: cycle notification preference (All -> Mentions -> None -> All) @@ -3631,6 +3804,28 @@ public: } } + // S: share channel with a contact via DM + if (c == 's' || c == 'S') { + if (_rows[_cursor].type == ROW_CHANNEL) { + _shareChannelIdx = _rows[_cursor].param; + // Populate contact list with DM-capable contacts + _shareContactCount = 0; + int numContacts = the_mesh.getNumContacts(); + for (int ci = 0; ci < numContacts && _shareContactCount < SHARE_MAX_CONTACTS; ci++) { + ContactInfo contact; + if (the_mesh.getContactByIdx(ci, contact) && contact.type == ADV_TYPE_CHAT) { + _shareContacts[_shareContactCount++] = ci; + } + } + _sharePickerIdx = 0; + _sharePickerScroll = 0; + _editMode = EDIT_SHARE_PICK; + Serial.printf("Settings: sharing channel %d, %d contacts available\n", + _shareChannelIdx, _shareContactCount); + return true; + } + } + // T: open notification tone picker #if defined(MECK_AUDIO_VARIANT) || defined(HAS_4G_MODEM) if (c == 't' || c == 'T') { diff --git a/variants/lilygo_techo_card/Cpupowermanager.h b/variants/lilygo_techo_card/Cpupowermanager.h deleted file mode 100644 index a0493cf7..00000000 --- a/variants/lilygo_techo_card/Cpupowermanager.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -// ============================================================================= -// CPUPowerManager — no-op stub for nRF52840 -// -// The nRF52840 does not support runtime CPU frequency scaling. -// This stub satisfies the #include in main.cpp without any effect. -// ============================================================================= - -class CPUPowerManager { -public: - void begin() {} - void setHighPerformance() {} - void setLowPower() {} - void loop() {} -}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/GPSStreamCounter.h b/variants/lilygo_techo_card/GPSStreamCounter.h deleted file mode 100644 index 4013e9fc..00000000 --- a/variants/lilygo_techo_card/GPSStreamCounter.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include - -// Transparent Stream wrapper that counts NMEA sentences (newline-delimited) -// flowing from the GPS serial port to the MicroNMEA parser. -// -// Usage: Instead of MicroNMEALocationProvider gps(Serial2, &rtc_clock); -// Use: GPSStreamCounter gpsStream(Serial2); -// MicroNMEALocationProvider gps(gpsStream, &rtc_clock); -// -// Every read() call passes through to the underlying stream; when a '\n' -// is seen the sentence counter increments. This lets the UI display a -// live "nmea" count so users can confirm the baud rate is correct and -// the GPS module is actually sending data. - -class GPSStreamCounter : public Stream { -public: - GPSStreamCounter(Stream& inner) - : _inner(inner), _sentences(0), _sentences_snapshot(0), - _last_snapshot(0), _sentences_per_sec(0) {} - - // --- Stream read interface (passes through) --- - int available() override { return _inner.available(); } - int peek() override { return _inner.peek(); } - - int read() override { - int c = _inner.read(); - if (c == '\n') { - _sentences++; - } - return c; - } - - // --- Stream write interface (pass through for NMEA commands if needed) --- - size_t write(uint8_t b) override { return _inner.write(b); } - - // Required override on Adafruit nRF52 BSP where Stream::flush() is pure virtual. - // No-op equivalent on ESP32 cores that provide a default implementation. - void flush() override { _inner.flush(); } - - // --- Sentence counting API --- - - // Total sentences received since boot (or last reset) - uint32_t getSentenceCount() const { return _sentences; } - - // Sentences received per second (updated each time you call it, - // with a 1-second rolling window) - uint16_t getSentencesPerSec() { - unsigned long now = millis(); - unsigned long elapsed = now - _last_snapshot; - if (elapsed >= 1000) { - uint32_t delta = _sentences - _sentences_snapshot; - // Scale to per-second if interval wasn't exactly 1000ms - _sentences_per_sec = (uint16_t)((delta * 1000UL) / elapsed); - _sentences_snapshot = _sentences; - _last_snapshot = now; - } - return _sentences_per_sec; - } - - // Reset all counters (e.g. when GPS hardware power cycles) - void resetCounters() { - _sentences = 0; - _sentences_snapshot = 0; - _sentences_per_sec = 0; - _last_snapshot = millis(); - } - -private: - Stream& _inner; - volatile uint32_t _sentences; - uint32_t _sentences_snapshot; - unsigned long _last_snapshot; - uint16_t _sentences_per_sec; -}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/Radiopresets.h b/variants/lilygo_techo_card/Radiopresets.h deleted file mode 100644 index 88a49d80..00000000 --- a/variants/lilygo_techo_card/Radiopresets.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -// --------------------------------------------------------------------------- -// Radio presets — shared between SettingsScreen (UI) and MyMesh (Serial CLI) -// --------------------------------------------------------------------------- - -struct RadioPreset { - const char* name; - float freq; - float bw; - uint8_t sf; - uint8_t cr; - uint8_t tx_power; -}; - -static const RadioPreset RADIO_PRESETS[] = { - { "Australia", 915.800f, 250.0f, 10, 5, 22 }, - { "Australia (Narrow)", 916.575f, 62.5f, 7, 8, 22 }, - { "Australia: SA, WA", 923.125f, 62.5f, 8, 8, 22 }, - { "Australia: QLD", 923.125f, 62.5f, 8, 5, 22 }, - { "EU/UK (Narrow)", 869.618f, 62.5f, 8, 8, 14 }, - { "EU/UK (Long Range)", 869.525f, 250.0f, 11, 5, 14 }, - { "EU/UK (Medium Range)", 869.525f, 250.0f, 10, 5, 14 }, - { "Czech Republic (Narrow)",869.432f, 62.5f, 7, 5, 14 }, - { "EU 433 (Long Range)", 433.650f, 250.0f, 11, 5, 14 }, - { "New Zealand", 917.375f, 250.0f, 11, 5, 22 }, - { "New Zealand (Narrow)", 917.375f, 62.5f, 7, 5, 22 }, - { "Portugal 433", 433.375f, 62.5f, 9, 6, 14 }, - { "Portugal 868", 869.618f, 62.5f, 7, 6, 14 }, - { "Switzerland", 869.618f, 62.5f, 8, 8, 14 }, - { "USA/Canada (Recommended)",910.525f, 62.5f, 7, 5, 22 }, - { "Vietnam", 920.250f, 250.0f, 11, 5, 22 }, -}; -#define NUM_RADIO_PRESETS (sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0])) \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp deleted file mode 100644 index 6fcc94c1..00000000 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ /dev/null @@ -1,339 +0,0 @@ -#include "TechoCardBoard.h" -#include "variant.h" -#include -#include -#include -using namespace Adafruit_LittleFS_Namespace; - -void TechoCardBoard::begin() { - NRF52BoardDCDC::begin(); - Serial.begin(115200); - - // RT9080 3V3 rail: clean reset cycle (from Meshtastic PR #10267) - // Toggling EN HIGH→LOW→HIGH forces a clean power-on, preventing - // brown-out when LoRa TX fires at full power. - #if PIN_OLED_EN >= 0 - pinMode(PIN_OLED_EN, OUTPUT); - digitalWrite(PIN_OLED_EN, HIGH); - delay(100); - digitalWrite(PIN_OLED_EN, LOW); - delay(100); - digitalWrite(PIN_OLED_EN, HIGH); - delay(100); - #endif - - // Park peripheral enable pins LOW before setup runs - #if defined(HAS_GPS) && PIN_GPS_EN >= 0 - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); - #endif - #if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0 - pinMode(PIN_GPS_RF_EN, OUTPUT); - digitalWrite(PIN_GPS_RF_EN, LOW); - #endif - #if defined(HAS_BUZZER) && PIN_BUZZER >= 0 - pinMode(PIN_BUZZER, OUTPUT); - digitalWrite(PIN_BUZZER, LOW); - #endif - #if defined(HAS_SPEAKER) - pinMode(PIN_SPK_EN, OUTPUT); - digitalWrite(PIN_SPK_EN, LOW); - #if PIN_SPK_EN2 >= 0 - pinMode(PIN_SPK_EN2, OUTPUT); - digitalWrite(PIN_SPK_EN2, LOW); - #endif - #endif - - // Enable GPS power after rail stabilises - #if defined(HAS_GPS) && PIN_GPS_EN >= 0 - delay(10); - digitalWrite(PIN_GPS_EN, HIGH); - #endif - #if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0 - digitalWrite(PIN_GPS_RF_EN, HIGH); - #endif - - // Initialise GPS UART - #if defined(HAS_GPS) - Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX); - Serial1.begin(GPS_BAUDRATE); - #endif - - pinMode(PIN_VBAT_READ, INPUT); - pinMode(PIN_USER_BTN, INPUT); - - // Initialise I2C -- must be done before display.begin() is called from main.cpp - Wire.begin(); - Wire.setClock(400000); - - // Initialise WS2812 NeoPixel chain (all off at boot) - // Force data line LOW before init to prevent stray HIGH latching green - #if defined(HAS_RGB_LED) - pinMode(PIN_RGB_LED_1, OUTPUT); - digitalWrite(PIN_RGB_LED_1, LOW); - delayMicroseconds(300); // WS2812 reset pulse is ~280us - _pixels.begin(); - _pixels.clear(); - _pixels.show(); - #endif -} - -void TechoCardBoard::enableGPS(bool enable) { - #if defined(HAS_GPS) && PIN_GPS_EN >= 0 - digitalWrite(PIN_GPS_EN, enable ? HIGH : LOW); - #endif - #if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0 - digitalWrite(PIN_GPS_RF_EN, enable ? HIGH : LOW); - #endif -} - -float TechoCardBoard::getMCUTemperature() { - // SoftDevice owns the TEMP peripheral -- direct register access hard faults. - // Use sd_temp_get() when SoftDevice is enabled. - int32_t temp; - uint8_t sd_en = 0; - sd_softdevice_is_enabled(&sd_en); - if (sd_en) { - if (sd_temp_get(&temp) == NRF_SUCCESS) { - return temp * 0.25f; - } - return NAN; - } - // SoftDevice off -- fall back to parent's direct register access - return NRF52Board::getMCUTemperature(); -} - -void TechoCardBoard::enableSpeaker(bool enable) { - #if defined(HAS_SPEAKER) - digitalWrite(PIN_SPK_EN, enable ? HIGH : LOW); - #if PIN_SPK_EN2 >= 0 - digitalWrite(PIN_SPK_EN2, enable ? HIGH : LOW); - #endif - #endif -} - -void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) { - #if defined(HAS_RGB_LED) - uint32_t color = Adafruit_NeoPixel::Color(r, g, b); - for (int i = 0; i < NUM_NEOPIXELS; i++) { - _pixels.setPixelColor(i, color); - } - _pixels.show(); - #else - (void)r; (void)g; (void)b; - #endif -} - -void TechoCardBoard::ledOff() { - setLED(0, 0, 0); -} - -void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) { - #if defined(HAS_RGB_LED) - if (led_index < NUM_NEOPIXELS) { - _pixels.setPixelColor(led_index, color); - _pixels.show(); - } - #else - (void)led_index; (void)color; - #endif -} - -void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) { - #if defined(HAS_BUZZER) && PIN_BUZZER >= 0 - if (freq_hz == 0 || duration_ms == 0) { - noTone(PIN_BUZZER); - return; - } - tone(PIN_BUZZER, freq_hz, duration_ms); - #else - (void)freq_hz; (void)duration_ms; - #endif -} - -// ============================================================================= -// BQ25896 Charger IC (I2C address 0x6B) -// ============================================================================= - -#define BQ25896_ADDR 0x6B - -bool TechoCardBoard::probeCharger() { - if (!_chargerProbed) { - Wire.beginTransmission(BQ25896_ADDR); - _chargerPresent = (Wire.endTransmission() == 0); - _chargerProbed = true; - if (!_chargerPresent) { - Serial.println("BQ25896: not found at 0x6B"); - } - } - return _chargerPresent; -} - -uint8_t TechoCardBoard::readChargerReg(uint8_t reg) { - if (!probeCharger()) return 0; - Wire.beginTransmission(BQ25896_ADDR); - Wire.write(reg); - if (Wire.endTransmission(false) != 0) return 0; - Wire.requestFrom((uint8_t)BQ25896_ADDR, (uint8_t)1); - return Wire.available() ? Wire.read() : 0; -} - -void TechoCardBoard::writeChargerReg(uint8_t reg, uint8_t val) { - Wire.beginTransmission(BQ25896_ADDR); - Wire.write(reg); - Wire.write(val); - Wire.endTransmission(); -} - -void TechoCardBoard::enableChargerADC() { - uint8_t reg02 = readChargerReg(0x02); - reg02 |= 0xC0; // CONV_RATE=1 (continuous) + CONV_START=1 - writeChargerReg(0x02, reg02); -} - -uint8_t TechoCardBoard::getChargeStatus() { - return (readChargerReg(0x0B) >> 3) & 0x03; -} - -uint16_t TechoCardBoard::getChargerBattMV() { - return 2304 + (readChargerReg(0x0E) & 0x7F) * 20; -} - -uint8_t TechoCardBoard::getChargerTSPCT() { - return 21 + (readChargerReg(0x10) & 0x7F); -} - -// ============================================================================= -// ICM20948 / AK09916 Compass -// -// Enable I2C bypass on the ICM20948 so the AK09916 magnetometer at 0x0C -// appears directly on Wire. Then set continuous measurement mode. -// ============================================================================= - -#define ICM20948_ADDR 0x68 -#define AK09916_ADDR 0x0C - -static uint8_t _i2c_rd(uint8_t addr, uint8_t reg) { - Wire.beginTransmission(addr); - Wire.write(reg); - if (Wire.endTransmission(false) != 0) return 0; - Wire.requestFrom(addr, (uint8_t)1); - return Wire.available() ? Wire.read() : 0; -} - -static void _i2c_wr(uint8_t addr, uint8_t reg, uint8_t val) { - Wire.beginTransmission(addr); - Wire.write(reg); - Wire.write(val); - Wire.endTransmission(); -} - -bool TechoCardBoard::initCompass() { - if (_compassReady) return true; - - // Bank 0 - _i2c_wr(ICM20948_ADDR, 0x7F, 0x00); - - // Check WHO_AM_I (expect 0xEA) - if (_i2c_rd(ICM20948_ADDR, 0x00) != 0xEA) return false; - - // Wake up: auto clock, not sleep - _i2c_wr(ICM20948_ADDR, 0x06, 0x01); - delay(10); - - // Enable I2C bypass so AK09916 is directly accessible - _i2c_wr(ICM20948_ADDR, 0x0F, 0x02); - delay(5); - - // Check AK09916 WHO_AM_I (expect 0x09) - if (_i2c_rd(AK09916_ADDR, 0x01) != 0x09) return false; - - // Leave in power-down -- readMag triggers single measurements on demand - _i2c_wr(AK09916_ADDR, 0x31, 0x00); - - _compassReady = true; - return true; -} - -bool TechoCardBoard::readMag(int16_t& mx, int16_t& my, int16_t& mz) { - if (!_compassReady) return false; - - // Single-measurement mode: trigger one fresh measurement per call. - // Continuous mode gets disrupted by OLED I2C display writes sharing - // the bus through ICM20948 bypass, causing stale data. - _i2c_wr(AK09916_ADDR, 0x31, 0x01); // single measurement trigger - - // Wait for data ready (measurement takes ~7.2ms) - for (int i = 0; i < 20; i++) { - if (_i2c_rd(AK09916_ADDR, 0x10) & 0x01) break; - delay(1); - } - - // Burst read 6 data bytes + ST2 (must read ST2 to complete cycle) - Wire.beginTransmission(AK09916_ADDR); - Wire.write(0x11); - if (Wire.endTransmission(false) != 0) return false; - Wire.requestFrom((uint8_t)AK09916_ADDR, (uint8_t)7); - if (Wire.available() < 7) return false; - - uint8_t buf[7]; - for (int i = 0; i < 7; i++) buf[i] = Wire.read(); - - mx = (int16_t)(buf[1] << 8 | buf[0]); - my = (int16_t)(buf[3] << 8 | buf[2]); - mz = (int16_t)(buf[5] << 8 | buf[4]); - // buf[6] = ST2, read to unlatch - - return true; -} - -// Power down the AK09916 magnetometer and put the ICM20948 itself to sleep. -// Saves ~3-4mA when not actively viewing the compass page. -// Next call to initCompass() will fully re-initialise the chain. -void TechoCardBoard::sleepCompass() { - if (!_compassReady) return; - - // Bank 0 (in case we drifted) - _i2c_wr(ICM20948_ADDR, 0x7F, 0x00); - - // AK09916 CNTL2 = 0x00 -- power-down mode (stops continuous measurement) - _i2c_wr(AK09916_ADDR, 0x31, 0x00); - - // ICM20948 PWR_MGMT_1 = 0x40 -- SLEEP bit set - _i2c_wr(ICM20948_ADDR, 0x06, 0x40); - - _compassReady = false; -} - -// ============================================================================= -// Compass calibration persistence -// ============================================================================= - -#define COMPASS_CAL_FILE "/compass_cal" - -bool TechoCardBoard::loadCalibration() { - // InternalFS must already be initialised (done in main.cpp setup) - File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_READ); - if (file) { - int n = file.read((uint8_t*)&_cal, sizeof(_cal)); - file.close(); - if (n == (int)sizeof(_cal) && _cal.magic == COMPASS_CAL_MAGIC) { - return true; - } - } - // No valid calibration -- reset to identity (no correction) - _cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 }; - return false; -} - -bool TechoCardBoard::saveCalibration(const CompassCalibration& cal) { - _cal = cal; - _cal.magic = COMPASS_CAL_MAGIC; - // Direct-write pattern: remove then create (nRF52 LittleFS compatible) - InternalFS.remove(COMPASS_CAL_FILE); - File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_WRITE); - if (!file) return false; - file.write((const uint8_t*)&_cal, sizeof(_cal)); - file.close(); - return true; -} \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h deleted file mode 100644 index ec8d4462..00000000 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once - -#include -#include -#include -#include "variant.h" - -#if defined(HAS_RGB_LED) - #include -#endif - -// Hard-iron offsets + soft-iron axis scaling. -// Computed by on-device calibration (rotate slowly for ~20 seconds). -// Persisted to /compass_cal on InternalFS. -#define COMPASS_CAL_MAGIC 0xCA1B0000 - -struct CompassCalibration { - int16_t off_x, off_y, off_z; // hard-iron offsets (raw ADC counts) - float scale_x, scale_y, scale_z; // soft-iron per-axis scale factors - uint32_t magic; // COMPASS_CAL_MAGIC when valid -}; - -class TechoCardBoard : public NRF52BoardDCDC { -private: - #if defined(HAS_RGB_LED) - Adafruit_NeoPixel _pixels = Adafruit_NeoPixel(NUM_NEOPIXELS, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800); - #endif - -public: - TechoCardBoard() {} - - void begin(); - - uint16_t getBattMilliVolts() override { - int adcvalue = 0; - analogReadResolution(12); - analogReference(AR_INTERNAL_3_0); - pinMode(PIN_BAT_CTL, OUTPUT); - pinMode(PIN_VBAT_READ, INPUT); - digitalWrite(PIN_BAT_CTL, HIGH); - - delay(10); - adcvalue = analogRead(PIN_VBAT_READ); - digitalWrite(PIN_BAT_CTL, LOW); - - return (uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER); - } - - const char* getManufacturerName() const override { - return "LilyGo T-Echo Card"; - } - - float getMCUTemperature() override; - - void powerOff() override { - sd_power_system_off(); - } - - // GPS power control - void enableGPS(bool enable); - - // Speaker power control - void enableSpeaker(bool enable); - - // RGB LEDs -- all three to same colour - void setLED(uint8_t r, uint8_t g, uint8_t b); - void ledOff(); - - // Per-LED status control (0=power, 1=notify, 2=pairing) - void setStatusLED(uint8_t led_index, uint32_t color); - - // Buzzer - void buzz(uint16_t freq_hz, uint16_t duration_ms); - - // BQ25896 charger IC (0x6B) - bool probeCharger(); // check if BQ25896 responds on I2C - uint8_t readChargerReg(uint8_t reg); - void writeChargerReg(uint8_t reg, uint8_t val); - void enableChargerADC(); // start continuous ADC conversion - uint8_t getChargeStatus(); // 0=none, 1=pre, 2=fast, 3=done - uint16_t getChargerBattMV(); // battery voltage from charger ADC - uint8_t getChargerTSPCT(); // thermistor voltage as % of REGN - - // ICM20948 / AK09916 compass (0x68 bypass to 0x0C) - bool initCompass(); - bool readMag(int16_t& mx, int16_t& my, int16_t& mz); - void sleepCompass(); // power down magnetometer + put ICM20948 in sleep mode - - // Compass calibration (persisted to InternalFS) - bool loadCalibration(); // call after InternalFS.begin() - bool saveCalibration(const CompassCalibration& cal); - bool isCalibrated() const { return _cal.magic == COMPASS_CAL_MAGIC; } - const CompassCalibration& getCalibration() const { return _cal; } - -private: - bool _compassReady = false; - bool _chargerProbed = false; - bool _chargerPresent = false; - CompassCalibration _cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 }; -}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h deleted file mode 100644 index 75291631..00000000 --- a/variants/lilygo_techo_card/TechoCardHomeScreen.h +++ /dev/null @@ -1,888 +0,0 @@ -// ============================================================================= -// TechoCardHomeScreen -- 72x40 OLED home screen for LilyGo T-Echo Card -// -// Four-line layout using U8g2's 4x6 tom_thumb font (18 chars x 4 lines). -// U8g2's native SSD1306_72X40_ER support handles all GDDRAM offset mapping. -// -// Two-button navigation: A (pin 42) = next page / long-press activate -// C (pin 24) = previous page -// -// Pages: STATUS -> RADIO -> BLE -> ADVERT -> GPS -> COMPASS -> BATTERY -> HIBERNATE -// ============================================================================= -#pragma once - -#include -#include -#include -#include -#include -#include "MyMesh.h" -#include "UITask.h" - -// ============================================================================= -// Voice recording -- PDM mic -> Codec2 1200bps stream encoding -// ============================================================================= -#if defined(HAS_MICROPHONE) - #include - #include - - // VE3 protocol constants (wire-compatible with ESP32 VoiceMessageScreen) - #define VC_C2_MODE CODEC2_MODE_1200 - #define VC_C2_MODE_ID 1 // Codec2 1200bps mode identifier - #define VC_C2_FRAME_MS 40 // 40ms per frame at 1200bps - #define VC_C2_FRAME_SAM 320 // 320 samples per frame at 8kHz - #define VC_C2_FRAME_BYTES 6 // 6 encoded bytes per frame - #define VC_MAX_SECONDS 5 - #define VC_MAX_FRAMES (VC_MAX_SECONDS * 1000 / VC_C2_FRAME_MS) // 125 - #define VC_MAX_BYTES (VC_MAX_FRAMES * VC_C2_FRAME_BYTES) // 750 - #define VC_MESH_PAYLOAD 150 // Usable codec2 bytes per mesh packet - #define VC_PKT_MAGIC 0x56 // Voice packet magic byte - #define VC_PKT_HDR_SIZE 6 // magic(1) + sessionId(4) + pktIdx(1) - #define VC_PDM_RATE 16000 - #define VC_PDM_FRAME (VC_C2_FRAME_SAM * 2) // 640 16kHz samples per codec frame - - // PDM ring buffer -- 2 codec frames of headroom at 16kHz - #define VC_PDM_BUF_SAMPLES (VC_PDM_FRAME * 2) // 1280 samples = 2560 bytes - - // Forward declaration for static callback - class TechoCardHomeScreen; - static TechoCardHomeScreen* _vcSelf = nullptr; - static void _vcPdmISR(); -#endif - -class TechoCardHomeScreen : public UIScreen { - enum Page { - STATUS, - RADIO, -#ifdef BLE_PIN_CODE - BLE, -#endif - ADVERT, -#if defined(HAS_MICROPHONE) - VOICE, -#endif -#if ENV_INCLUDE_GPS == 1 - GPS, -#endif - COMPASS, - BATTERY, - HIBERNATE, - PAGE_COUNT - }; - - UITask* _task; - mesh::RTCClock* _rtc; - NodePrefs* _prefs; - uint8_t _page; - bool _shutdown_init; - unsigned long _shutdown_at; - - // Compass state - bool _compassInitDone; - bool _compassOK; - float _lastHeading; - int16_t _lastMx, _lastMy, _lastMz; - - // Compass calibration state - bool _calMode; - unsigned long _calStart; - uint16_t _calCount; - int16_t _calMinX, _calMaxX; - int16_t _calMinY, _calMaxY; - int16_t _calMinZ, _calMaxZ; - - // Diagnostic counters (temporary) - uint16_t _magOk; - uint16_t _magFail; - -#if defined(HAS_MICROPHONE) - // Voice recording state - enum VoiceState { V_IDLE, V_RECORDING, V_REVIEW }; - VoiceState _vState; - struct CODEC2* _vCodec; - - // PDM sample accumulator (filled by ISR, consumed by poll) - int16_t _vPdmBuf[VC_PDM_BUF_SAMPLES]; - volatile int _vPdmCount; - volatile uint32_t _vIsrCount; - - // Codec2 encoded output - uint8_t _vEncoded[VC_MAX_BYTES]; // 750 bytes max - uint16_t _vEncBytes; - uint16_t _vEncFrames; - unsigned long _vRecStart; - - // VE3 outgoing session - uint32_t _vSessionId; - bool _vSessionActive; -#endif - - // Four lines at 9px spacing within 40px display. - // U8g2 handles panel offset natively -- y=0 is the true visible top. - static const int Y0 = 2; - static const int Y1 = 11; - static const int Y2 = 20; - static const int Y3 = 29; - - int battPercent() { - uint16_t mv = _task->getBattMilliVolts(); - if (mv == 0) return 0; - int pct = ((int)mv - 3000) * 100 / 1160; - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - return pct; - } - - const char* cardinal(float deg) { - if (deg >= 337.5f || deg < 22.5f) return "N"; - if (deg < 67.5f) return "NE"; - if (deg < 112.5f) return "E"; - if (deg < 157.5f) return "SE"; - if (deg < 202.5f) return "S"; - if (deg < 247.5f) return "SW"; - if (deg < 292.5f) return "W"; - return "NW"; - } - -public: - TechoCardHomeScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* prefs) - : _task(task), _rtc(rtc), _prefs(prefs), - _page(STATUS), _shutdown_init(false), _shutdown_at(0), - _compassInitDone(false), _compassOK(false), - _lastHeading(0), _lastMx(0), _lastMy(0), _lastMz(0), - _calMode(false), _calStart(0), _calCount(0), - _calMinX(0), _calMaxX(0), - _calMinY(0), _calMaxY(0), - _calMinZ(0), _calMaxZ(0), - _magOk(0), _magFail(0) -#if defined(HAS_MICROPHONE) - , _vState(V_IDLE), _vCodec(nullptr), _vPdmCount(0), _vIsrCount(0), - _vEncBytes(0), _vEncFrames(0), _vRecStart(0), - _vSessionId(0), _vSessionActive(false) -#endif - {} - - void cancelEditing() { _shutdown_init = false; } - -#if defined(HAS_MICROPHONE) - // --- Voice recording helpers --- - - void onPDMData() { - int avail = PDM.available(); - if (avail <= 0) return; - int samples = avail / (int)sizeof(int16_t); - int space = VC_PDM_BUF_SAMPLES - _vPdmCount; - if (samples > space) samples = space; - if (samples > 0) { - PDM.read(&_vPdmBuf[_vPdmCount], samples * sizeof(int16_t)); - _vPdmCount += samples; - _vIsrCount++; - } - } - - bool voiceStartRecording() { - if (_vState == V_RECORDING) return false; - - // Codec2 created lazily on first VOICE page visit (see render) - if (!_vCodec) { - Serial.println("Voice: no codec2 instance!"); - return false; - } - - // Reset buffers - _vPdmCount = 0; - _vIsrCount = 0; - _vEncBytes = 0; - _vEncFrames = 0; - - // Enable speaker amp power rail (RT9080 powers both speaker and mic) - Serial.println("Voice: enabling speaker rail..."); - board.enableSpeaker(true); - delay(50); - - // Start PDM capture - Serial.println("Voice: starting PDM..."); - _vcSelf = this; - PDM.setPins(PIN_MIC_DATA, PIN_MIC_CLK, -1); - PDM.onReceive(_vcPdmISR); - if (!PDM.begin(1, VC_PDM_RATE)) { - Serial.println("Voice: PDM.begin failed"); - codec2_destroy(_vCodec); - _vCodec = nullptr; - board.enableSpeaker(false); - return false; - } - PDM.setGain(80); - Serial.println("Voice: PDM started OK"); - - _vState = V_RECORDING; - _vRecStart = millis(); - Serial.println("Voice: Recording started"); - return true; - } - - void voiceStopRecording() { - if (_vState != V_RECORDING) return; - - // Stop PDM - PDM.end(); - _vcSelf = nullptr; - - // Encode any remaining samples - voiceProcessSamples(); - - // Keep Codec2 alive -- don't destroy between recordings - // (destroying and re-creating causes heap fragmentation) - // if (_vCodec) { codec2_destroy(_vCodec); _vCodec = nullptr; } - - // Power down mic/speaker rail - board.enableSpeaker(false); - - unsigned long dur = millis() - _vRecStart; - Serial.printf("Voice: Stopped -- %d frames, %d bytes, %lums\n", - _vEncFrames, _vEncBytes, dur); - - _vState = (_vEncFrames > 0) ? V_REVIEW : V_IDLE; - } - - // Process accumulated PDM samples into Codec2 frames. - // Called from poll() during recording. - void voiceProcessSamples() { - if (_vPdmCount < VC_PDM_FRAME) return; - if (_vEncBytes + VC_C2_FRAME_BYTES > VC_MAX_BYTES) return; - - // TEST MODE: drain PDM buffer WITHOUT Codec2 encoding - // If this works, PDM pipeline is fine and issue is Codec2 - int remaining = _vPdmCount - VC_PDM_FRAME; - if (remaining > 0) { - memmove((void*)_vPdmBuf, (const void*)&_vPdmBuf[VC_PDM_FRAME], - remaining * sizeof(int16_t)); - } - _vPdmCount = remaining; - _vEncFrames++; - _vEncBytes += VC_C2_FRAME_BYTES; // fake it for display - } - - // VE3 base36 encoding (compact wire format) - static int toBase36(uint32_t val, char* buf, int bufLen) { - static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; - if (bufLen < 2) return 0; - if (val == 0) { buf[0] = '0'; buf[1] = '\0'; return 1; } - char tmp[12]; int pos = 0; - while (val > 0 && pos < 11) { tmp[pos++] = digits[val % 36]; val /= 36; } - if (pos >= bufLen) pos = bufLen - 1; - for (int i = 0; i < pos; i++) buf[i] = tmp[pos - 1 - i]; - buf[pos] = '\0'; - return pos; - } - - // --- Public voice API for main.cpp --- -public: - // Codec2 must be created from main loop (shallow stack). - // render() call chain is too deep for codec2_create's 3KB+ stack needs. - bool needsCodec2() const { -#if defined(HAS_MICROPHONE) - return _page == VOICE && !_vCodec; -#else - return false; -#endif - } - void setCodec2Instance(struct CODEC2* c2) { -#if defined(HAS_MICROPHONE) - _vCodec = c2; -#endif - } - - bool isVoiceReview() const { -#if defined(HAS_MICROPHONE) - return _vState == V_REVIEW && _vEncFrames > 0; -#else - return false; -#endif - } - - // Format VE3 envelope: "VE3:{sid}:{mode}:{total}:{dur}" - void voiceFormatEnvelope(char* buf, int bufLen, uint32_t sessionId) { - int payloadPerPkt = VC_MESH_PAYLOAD; - uint8_t totalPkts = (_vEncBytes + payloadPerPkt - 1) / payloadPerPkt; - uint8_t durSec = (uint8_t)(_vEncFrames * VC_C2_FRAME_MS / 1000); - char sid[12], mode[4], total[4], dur[4]; - toBase36(sessionId, sid, sizeof(sid)); - toBase36(VC_C2_MODE_ID, mode, sizeof(mode)); - toBase36(totalPkts, total, sizeof(total)); - toBase36(durSec, dur, sizeof(dur)); - snprintf(buf, bufLen, "VE3:%s:%s:%s:%s", sid, mode, total, dur); - - // Cache session - _vSessionId = sessionId; - _vSessionActive = true; - } - - int voiceBuildPacket(uint8_t* buf, int bufLen, uint32_t sessionId, uint8_t pktIdx) { - if (!_vSessionActive || _vSessionId != sessionId) return 0; - uint32_t offset = (uint32_t)pktIdx * VC_MESH_PAYLOAD; - if (offset >= _vEncBytes) return 0; - uint32_t chunkLen = _vEncBytes - offset; - if (chunkLen > VC_MESH_PAYLOAD) chunkLen = VC_MESH_PAYLOAD; - if ((int)(VC_PKT_HDR_SIZE + chunkLen) > bufLen) return 0; - buf[0] = VC_PKT_MAGIC; - memcpy(&buf[1], &sessionId, 4); - buf[5] = pktIdx; - memcpy(&buf[6], &_vEncoded[offset], chunkLen); - return VC_PKT_HDR_SIZE + chunkLen; - } - - uint8_t voiceGetPacketCount() const { - if (!_vSessionActive) return 0; - return (_vEncBytes + VC_MESH_PAYLOAD - 1) / VC_MESH_PAYLOAD; - } - - void voiceOnSendComplete() { - _vSessionActive = false; - _vState = V_IDLE; - _vEncBytes = 0; - _vEncFrames = 0; - } - - void voiceDiscard() { - _vSessionActive = false; - _vState = V_IDLE; - _vEncBytes = 0; - _vEncFrames = 0; - } -private: -#endif // HAS_MICROPHONE - - int render(DisplayDriver& display) override { - char tmp[32]; - display.setTextSize(1); - - switch (_page) { - - // ----- STATUS ----- - case STATUS: { - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - char filtered_name[sizeof(_prefs->node_name)]; - display.translateUTF8ToBlocks(filtered_name, _prefs->node_name, - sizeof(filtered_name)); - display.print(filtered_name); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "MSG: %d", _task->getMsgCount()); - display.print(tmp); - - snprintf(tmp, sizeof(tmp), "%d%%", battPercent()); - display.drawTextRightAlign(display.width() - 1, Y1, tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - if (_task->hasConnection()) { - display.print("Connected"); - } else if (_task->isSerialEnabled()) { - display.print("BLE: On"); - } else { - display.print("BLE: Off"); - } - break; - } - - // ----- RADIO ----- - case RADIO: { - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y0); - snprintf(tmp, sizeof(tmp), "%.1f MHz SF%d", - _prefs->freq, _prefs->sf); - display.print(tmp); - - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "BW %.0f CR %d", - _prefs->bw, _prefs->cr); - display.print(tmp); - - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "TX: %d dBm", - _prefs->tx_power_dbm); - display.print(tmp); - - display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "NF: %d", - radio_driver.getNoiseFloor()); - display.print(tmp); - break; - } - -#ifdef BLE_PIN_CODE - // ----- BLE ----- - case BLE: { - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print(_task->isSerialEnabled() ? "BLE: ON" : "BLE: OFF"); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "PIN: %lu", - (unsigned long)the_mesh.getBLEPin()); - display.print(tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y3); - display.print("Hold A: toggle"); - break; - } -#endif - - // ----- ADVERT ----- - case ADVERT: { - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Advert"); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - display.print("Hold A: send"); - break; - } - -#if defined(HAS_MICROPHONE) - // ----- VOICE ----- - case VOICE: { - switch (_vState) { - case V_IDLE: - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Voice"); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - display.print("Hold A: record"); - break; - - case V_RECORDING: { - unsigned long elapsed = (millis() - _vRecStart) / 1000; - int remaining = VC_MAX_SECONDS - (int)elapsed; - if (remaining < 0) remaining = 0; - - display.setColor(DisplayDriver::RED); - display.setCursor(0, Y0); - display.print("RECORDING"); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "%ds left", remaining); - display.print(tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "Frames: %d", _vEncFrames); - display.print(tmp); - - display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "%d bytes", _vEncBytes); - display.print(tmp); - - return 200; // Fast refresh during recording - } - - case V_REVIEW: { - float durSec = _vEncFrames * VC_C2_FRAME_MS / 1000.0f; - int packets = (_vEncBytes + VC_MESH_PAYLOAD - 1) / VC_MESH_PAYLOAD; - - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Review"); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "%.1fs %d pkt", durSec, packets); - display.print(tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "%d bytes", _vEncBytes); - display.print(tmp); - - display.setCursor(0, Y3); - display.print("A:disc C:disc"); - break; - } - } // voice state switch - break; - } -#endif - -#if ENV_INCLUDE_GPS == 1 - // ----- GPS ----- - case GPS: { - LocationProvider* loc = sensors.getLocationProvider(); - - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - if (!_prefs->gps_enabled) { - display.print("GPS: OFF"); - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - display.print("Hold A: toggle"); - break; - } - - display.print("GPS: ON"); - if (loc) { - snprintf(tmp, sizeof(tmp), "S: %d", - loc->satellitesCount()); - display.drawTextRightAlign(display.width() - 1, Y0, tmp); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - display.print(loc->isValid() ? "Fix: 3D" : "No fix"); - - if (loc->isValid()) { - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "%.4f", - loc->getLatitude() / 1000000.0); - display.print(tmp); - - display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "%.4f", - loc->getLongitude() / 1000000.0); - display.print(tmp); - } else { - // No fix yet -- show NMEA sentence rate to confirm the chip is talking. - // If this stays at 0, GPS is silent (baud rate wrong, RF off, etc). - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "NMEA: %u/s", - (unsigned)gpsStream.getSentencesPerSec()); - display.print(tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y3); - display.print("Hold A: toggle"); - } - } - break; - } -#endif - - // ----- COMPASS ----- - case COMPASS: { - if (!_compassInitDone) { - _compassOK = board.initCompass(); - board.loadCalibration(); - _compassInitDone = true; - } - - // --- Calibration mode --- - if (_calMode) { - int16_t mx, my, mz; - if (_compassOK && board.readMag(mx, my, mz)) { - if (_calCount == 0) { - _calMinX = _calMaxX = mx; - _calMinY = _calMaxY = my; - _calMinZ = _calMaxZ = mz; - } else { - if (mx < _calMinX) _calMinX = mx; - if (mx > _calMaxX) _calMaxX = mx; - if (my < _calMinY) _calMinY = my; - if (my > _calMaxY) _calMaxY = my; - if (mz < _calMinZ) _calMinZ = mz; - if (mz > _calMaxZ) _calMaxZ = mz; - } - _calCount++; - } - - int spreadX = _calMaxX - _calMinX; - int spreadY = _calMaxY - _calMinY; - int spreadZ = _calMaxZ - _calMinZ; - unsigned long elapsed = millis() - _calStart; - bool adequate = (spreadX >= 100 && spreadY >= 100 && _calCount >= 150); - bool timeout = (elapsed >= 30000); - - if (adequate || (timeout && spreadX >= 50 && spreadY >= 50)) { - // Compute and save calibration - CompassCalibration cal; - cal.off_x = (_calMinX + _calMaxX) / 2; - cal.off_y = (_calMinY + _calMaxY) / 2; - cal.off_z = (_calMinZ + _calMaxZ) / 2; - float avgRange = ((float)spreadX + (float)spreadY) / 2.0f; - cal.scale_x = (spreadX > 0) ? avgRange / (float)spreadX : 1.0f; - cal.scale_y = (spreadY > 0) ? avgRange / (float)spreadY : 1.0f; - cal.scale_z = (spreadZ > 30) ? avgRange / (float)spreadZ : 1.0f; - cal.magic = COMPASS_CAL_MAGIC; - board.saveCalibration(cal); - _calMode = false; - _task->showAlert("Cal saved!", 800); - return 500; - } - - if (timeout) { - _calMode = false; - _task->showAlert("Try again", 800); - return 500; - } - - // Calibration progress display - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Calibrate"); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - display.print("Rotate slowly..."); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "Samples: %u", _calCount); - display.print(tmp); - - display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "X:%d Y:%d", spreadX, spreadY); - display.print(tmp); - - return 100; // fast sample collection - } - - // --- Normal compass display --- - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Compass"); - if (board.isCalibrated()) { - display.drawTextRightAlign(display.width() - 1, Y0, "CAL"); - } - - if (!_compassOK) { - display.setColor(DisplayDriver::RED); - display.setCursor(0, Y2); - display.print("IMU not found"); - break; - } - - int16_t mx, my, mz; - if (board.readMag(mx, my, mz)) { - _magOk++; - // Exponential moving average: 7/8 old + 1/8 new (settles in ~2s) - if (_magOk == 1) { - _lastMx = mx; _lastMy = my; _lastMz = mz; - } else { - _lastMx = (_lastMx * 7 + mx + 4) >> 3; - _lastMy = (_lastMy * 7 + my + 4) >> 3; - _lastMz = (_lastMz * 7 + mz + 4) >> 3; - } - float cx = (float)_lastMx; - float cy = (float)_lastMy; - if (board.isCalibrated()) { - const CompassCalibration& cal = board.getCalibration(); - cx = ((float)_lastMx - cal.off_x) * cal.scale_x; - cy = ((float)_lastMy - cal.off_y) * cal.scale_y; - } - // Y axis is inverted relative to compass convention on this PCB - _lastHeading = atan2f(-cy, cx) * 180.0f / (float)M_PI; - if (_lastHeading < 0) _lastHeading += 360.0f; - } else { - _magFail++; - } - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "%.0f %s", - _lastHeading, cardinal(_lastHeading)); - display.print(tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "X:%d Y:%d", _lastMx, _lastMy); - display.print(tmp); - - display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "Z:%d", _lastMz); - display.print(tmp); - - return 250; // smooth readable refresh - } - - // ----- BATTERY ----- - case BATTERY: { - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Battery"); - - uint16_t mv = _task->getBattMilliVolts(); - snprintf(tmp, sizeof(tmp), "%d%%", battPercent()); - display.drawTextRightAlign(display.width() - 1, Y0, tmp); - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y1); - snprintf(tmp, sizeof(tmp), "%d.%02dV", mv / 1000, (mv % 1000) / 10); - display.print(tmp); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - { - float dieTemp = board.getMCUTemperature(); - snprintf(tmp, sizeof(tmp), "Temp: %.0fC", dieTemp); - display.print(tmp); - } - break; - } - - // ----- HIBERNATE ----- - case HIBERNATE: { - if (_shutdown_init) { - display.setColor(DisplayDriver::RED); - display.setCursor(0, Y1); - display.print("Shutting down..."); - return 200; - } - - display.setColor(DisplayDriver::YELLOW); - display.setCursor(0, Y0); - display.print("Hibernate"); - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y2); - display.print("Hold A: sleep"); - break; - } - } // switch - - return 5000; - } - - bool handleInput(char c) override { - if (_shutdown_init) { - _shutdown_init = false; - return true; - } - - // Any input during calibration cancels it - if (_calMode) { - _calMode = false; - _task->showAlert("Cancelled", 500); - return true; - } - -#if defined(HAS_MICROPHONE) - // During recording: any button press stops recording - if (_vState == V_RECORDING) { - voiceStopRecording(); - return true; - } -#endif - - if (c == KEY_NEXT || c == 'd') { - _page = (_page + 1) % PAGE_COUNT; - return true; - } - if (c == KEY_PREV || c == 'a') { - _page = (_page + PAGE_COUNT - 1) % PAGE_COUNT; - return true; - } - - if (c == KEY_ENTER) { - switch (_page) { -#ifdef BLE_PIN_CODE - case BLE: - if (_task->isSerialEnabled()) { - _task->disableSerial(); - _task->showAlert("BLE Off", 800); - } else { - _task->enableSerial(); - _task->showAlert("BLE On", 800); - } - return true; -#endif - - case ADVERT: - _task->notify(UIEventType::ack); - if (the_mesh.advert()) { - _task->showAlert("Sent!", 800); - } else { - _task->showAlert("Failed", 800); - } - return true; - -#if defined(HAS_MICROPHONE) - case VOICE: - if (_vState == V_IDLE) { - if (voiceStartRecording()) { - return true; - } else { - _task->showAlert("Mic fail", 800); - return true; - } - } else if (_vState == V_RECORDING) { - voiceStopRecording(); - return true; - } else if (_vState == V_REVIEW) { - voiceDiscard(); - return true; - } - return false; -#endif - -#if ENV_INCLUDE_GPS == 1 - case GPS: - _task->toggleGPS(); - return true; -#endif - - case COMPASS: - if (!_compassOK) return false; - _calMode = true; - _calStart = millis(); - _calCount = 0; - return true; - - case HIBERNATE: - _shutdown_init = true; - _shutdown_at = millis() + 500; - return true; - - default: - return false; - } - } - - return false; - } - - void poll() override { - if (_shutdown_init && millis() >= _shutdown_at) { - if (!_task->isButtonPressed()) { - _task->shutdown(); - } - } - -#if defined(HAS_MICROPHONE) - // Stream-encode PDM samples during recording - if (_vState == V_RECORDING) { - // Periodic diagnostic — is PDM delivering data? - static unsigned long lastDiag = 0; - static uint32_t pollCount = 0; - pollCount++; - if (millis() - lastDiag > 500) { - lastDiag = millis(); - Serial.printf("Voice poll #%lu: isr=%lu pdm=%d frames=%d\n", - (unsigned long)pollCount, (unsigned long)_vIsrCount, - _vPdmCount, _vEncFrames); - } - - voiceProcessSamples(); - - // Auto-stop at max duration - if (_vEncFrames >= VC_MAX_FRAMES || - (millis() - _vRecStart) >= (unsigned long)(VC_MAX_SECONDS * 1000 + 200)) { - voiceStopRecording(); - } - } -#endif - } -}; - -// Static PDM callback -- must be defined after class so onPDMData() is visible -#if defined(HAS_MICROPHONE) -static void _vcPdmISR() { - if (_vcSelf) _vcSelf->onPDMData(); -} -#endif \ No newline at end of file diff --git a/variants/lilygo_techo_card/pins_arduino.h b/variants/lilygo_techo_card/pins_arduino.h deleted file mode 100644 index f89e92f0..00000000 --- a/variants/lilygo_techo_card/pins_arduino.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -// ============================================================================= -// Arduino pin compatibility header for LilyGo T-Echo Card -// -// This file provides Arduino-standard pin name aliases for the nRF52840 GPIOs. -// Only needed if creating a custom board variant inside the Adafruit nRF52 -// Arduino framework package. If using build flag overrides in platformio.ini, -// this file is optional. -// -// Pin mapping cross-referenced against: -// - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h -// - Meshtastic PR #10267 (caveman99 T-Echo-Card support) -// ============================================================================= - -// On nRF52840, Arduino digital pin numbers map 1:1 to nRF GPIO numbers -// (0–47 for port 0 and port 1). - -// LED — WS2812 addressable (no plain GPIO LED on this board) -#define LED_BUILTIN PIN_LED1 -#define PIN_LED1 39 // WS2812 RGB LED data 1 (1, 7) -#define LED_STATE_ON 1 - -// Buttons -#define PIN_BUTTON1 42 // Button A — orange front button (1, 10) -#define PIN_BUTTON2 24 // Boot button (0, 24) - -// Serial (USB CDC) -// nRF52840 native USB — no UART pin assignment needed for Serial -// Serial1 is used for GPS -#define PIN_SERIAL1_RX 21 // GPS TX → nRF RX (0, 21) -#define PIN_SERIAL1_TX 19 // nRF TX → GPS RX (0, 19) - -// I2C -#define PIN_WIRE_SDA 36 // (1, 4) -#define PIN_WIRE_SCL 34 // (1, 2) - -// SPI (LoRa — directly mapped, RadioLib handles pin control) -#define PIN_SPI_MISO 17 // (0, 17) -#define PIN_SPI_MOSI 15 // (0, 15) -#define PIN_SPI_SCK 13 // (0, 13) - -// Analog -#define PIN_A0 2 // (0, 2) — Battery ADC / AIN0 - -// QSPI Flash — ZD25WQ32CEIGR 4MB -// Confirmed from LilyGo t_echo_card_config.h and Meshtastic PR #10267. -// These are on a dedicated SPI bus, separate from LoRa SPI. -#define PIN_QSPI_SCK 4 // (0, 4) -#define PIN_QSPI_CS 12 // (0, 12) -#define PIN_QSPI_IO0 6 // (0, 6) — MOSI / D0 -#define PIN_QSPI_IO1 8 // (0, 8) — MISO / D1 -#define PIN_QSPI_IO2 41 // (1, 9) — WP / D2 -#define PIN_QSPI_IO3 26 // (0, 26) — HOLD / D3 - -// NFC (dedicated nRF52840 NFC pins — not GPIO-assignable) -// NFC1 = P0.09, NFC2 = P0.10 -// These are only usable as NFC when NFC is enabled in UICR. -// If NFC is disabled, they become GPIO9 and GPIO10. - -// PDM Microphone -#define PIN_PDM_CLK 35 // (1, 3) -#define PIN_PDM_DIN 37 // (1, 5) - -// I2S Speaker (MAX98357) -#define PIN_I2S_SCK 16 // BCLK (0, 16) -#define PIN_I2S_LRCK 22 // LRCK / WS (0, 22) -#define PIN_I2S_SDOUT 20 // DATA (0, 20) diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini deleted file mode 100644 index d41fb624..00000000 --- a/variants/lilygo_techo_card/platformio.ini +++ /dev/null @@ -1,154 +0,0 @@ -; ============================================================================= -; LilyGo T-Echo Card -- nRF52840 + SX1262 + SSD1306 OLED (72x40) + L76K GPS -; ============================================================================= - -[lilygo_techo_card] -extends = nrf52_base -board = lilygo_techo_card -platform_packages = framework-arduinoadafruitnrf52 -board_build.ldscript = boards/nrf52840_s140_v6.ld -extra_scripts = - create-uf2.py - arch/nrf52/extra_scripts/patch_bluefruit.py - pre:patch_nrf52_bsp.py -; Point FrameworkArduinoVariant at a directory containing ONLY variant.h/cpp. -; Without this, PlatformIO tries to compile TechoCardBoard.cpp and target.cpp -; as part of the framework variant, which fails because MeshCore.h and -; RadioLib.h aren't on the BSP include path. -board_build.variants_dir = variants_bsp -build_flags = ${nrf52_base.build_flags} - -I src/helpers/nrf52 - -I lib/nrf52/s140_nrf52_6.1.1_API/include - -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 - -I variants/lilygo_techo_card - -I lib/PDM_nrf52/src - -I lib/codec2_nrf52/src - -D LILYGO_TECHO_CARD - -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=140 - -D SX126X_RX_BOOSTED_GAIN=1 - -D USE_U8G2_DISPLAY - -D DISPLAY_CLASS=U8g2Display - -D PIN_BUZZER=77 - -D PIN_BOOT_BTN=24 - -D ENV_INCLUDE_GPS=1 - -D ENV_SKIP_GPS_DETECT - -D ps_calloc=calloc - -D DISABLE_DIAGNOSTIC_OUTPUT - -D AUTO_SHUTDOWN_MILLIVOLTS=2980 -build_src_filter = ${nrf52_base.build_src_filter} - + - + - + - +<../variants/lilygo_techo_card> - +<../lib/PDM_nrf52/src> - +<../lib/PDM_nrf52/src/utility> - +<../lib/codec2_nrf52/src> -lib_compat_mode = off -lib_ldf_mode = deep+ -lib_deps = - ${nrf52_base.lib_deps} - stevemarple/MicroNMEA @ ^2.0.6 - adafruit/Adafruit NeoPixel @ ^1.12.3 - adafruit/Adafruit SSD1306 @ ^2.5.12 - adafruit/Adafruit GFX Library @ ^1.11.11 - adafruit/Adafruit BusIO @ ^1.16.2 - end2endzone/NonBlockingRtttl @ ^1.3.0 - olikraus/U8g2 @ ^2.35.19 - -debug_tool = jlink -upload_protocol = nrfutil - - -[env:techo_card_companion_radio_ble] -extends = lilygo_techo_card -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${lilygo_techo_card.build_flags} - -I examples/companion_radio - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=250 - -D MAX_GROUP_CHANNELS=4 - -D BLE_PIN_CODE=123456 - -D OFFLINE_QUEUE_SIZE=16 - -D CHANNEL_MSG_HISTORY_SIZE=1 - -D AUTO_OFF_MILLIS=60000 - -D ADVERT_PATH_TABLE_SIZE=200 -; -D BLE_DEBUG_LOGGING=1 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${lilygo_techo_card.build_src_filter} - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${lilygo_techo_card.lib_deps} - densaugeo/base64 @ ~1.4.0 - - -[env:techo_card_companion_radio_usb] -extends = lilygo_techo_card -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${lilygo_techo_card.build_flags} - -I examples/companion_radio - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 - -D AUTO_OFF_MILLIS=0 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${lilygo_techo_card.build_src_filter} - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${lilygo_techo_card.lib_deps} - densaugeo/base64 @ ~1.4.0 - - -[env:techo_card_repeater] -extends = lilygo_techo_card -build_flags = - ${lilygo_techo_card.build_flags} - -D ADVERT_NAME='"T-Echo Card Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=50 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${lilygo_techo_card.build_src_filter} - +<../examples/simple_repeater> - - -[env:techo_card_room_server] -extends = lilygo_techo_card -build_flags = - ${lilygo_techo_card.build_flags} - -D ADVERT_NAME='"T-Echo Card Room"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D ROOM_PASSWORD='"hello"' -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${lilygo_techo_card.build_src_filter} - +<../examples/simple_room_server> - - -[env:techo_card_sensor] -extends = lilygo_techo_card -build_flags = - ${lilygo_techo_card.build_flags} - -D ADVERT_NAME='"T-Echo Card Sensor"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 -build_src_filter = ${lilygo_techo_card.build_src_filter} - +<../examples/simple_sensor> \ No newline at end of file diff --git a/variants/lilygo_techo_card/sd.h b/variants/lilygo_techo_card/sd.h deleted file mode 100644 index bd728bde..00000000 --- a/variants/lilygo_techo_card/sd.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -// ============================================================================= -// SD.h -- nRF52 shim for T-Echo Card -// -// Maps the Arduino SD library API to Adafruit InternalFileSystem so that -// screen headers (NotesScreen, MapScreen, TextReaderScreen, EpubZipReader) -// compile without modification. The T-Echo Card has no SD card slot -- -// all file I/O goes to the 4MB QSPI flash via LittleFS. -// -// File class is already provided by the Adafruit LittleFS BSP. -// ============================================================================= - -#include - -class SDClass { -public: - bool begin(int cs = -1) { - (void)cs; - return true; // InternalFS is initialised in main.cpp setup() - } - - File open(const char* path, uint8_t mode = FILE_O_READ) { - return InternalFS.open(path, mode); - } - - bool exists(const char* path) { - return InternalFS.exists(path); - } - - bool mkdir(const char* path) { - return InternalFS.mkdir(path); - } - - bool remove(const char* path) { - return InternalFS.remove(path); - } - - bool rename(const char* from, const char* to) { - return InternalFS.rename(from, to); - } -}; - -static SDClass SD; \ No newline at end of file diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp deleted file mode 100644 index 43e79f4e..00000000 --- a/variants/lilygo_techo_card/target.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include -#include "target.h" -#include -#include - -TechoCardBoard board; - -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); - -WRAPPER_CLASS radio_driver(radio, board); - -VolatileRTCClock fallback_clock; -AutoDiscoverRTCClock rtc_clock(fallback_clock); - -#if ENV_INCLUDE_GPS - GPSStreamCounter gpsStream(Serial1); - MicroNMEALocationProvider gps(gpsStream, &rtc_clock); - EnvironmentSensorManager sensors(gps); -#else - EnvironmentSensorManager sensors; -#endif - -#ifdef DISPLAY_CLASS - DISPLAY_CLASS display; - MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); -#endif - -bool radio_init() { - // board.begin() and display.begin() are called by main.cpp before this. - // radio_init() should ONLY initialise the radio -- matching Meshpocket pattern. - return radio.std_init(&SPI); -} - -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(int8_t dbm) { - radio.setOutputPower(dbm); -} - -void radio_reset_agc() { -#ifdef SX126X_RX_BOOSTED_GAIN - radio.setRxBoostedGainMode(true); -#endif -} - -mesh::LocalIdentity radio_new_identity() { - RadioNoiseListener rng(radio); - return mesh::LocalIdentity(&rng); -} \ No newline at end of file diff --git a/variants/lilygo_techo_card/target.h b/variants/lilygo_techo_card/target.h deleted file mode 100644 index e18cc7fd..00000000 --- a/variants/lilygo_techo_card/target.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#define RADIOLIB_STATIC_ONLY 1 -#include -#include -#include -#include -#include -#include -#include "TechoCardBoard.h" - -#if ENV_INCLUDE_GPS -#include "GPSStreamCounter.h" -#endif - -#ifdef DISPLAY_CLASS - #if defined(USE_U8G2_DISPLAY) - #include - #else - #include - #endif - #include -#endif - -extern TechoCardBoard board; -extern WRAPPER_CLASS radio_driver; -extern AutoDiscoverRTCClock rtc_clock; - -#ifdef DISPLAY_CLASS - extern DISPLAY_CLASS display; - extern MomentaryButton user_btn; -#endif - -extern EnvironmentSensorManager sensors; - -#if ENV_INCLUDE_GPS -extern GPSStreamCounter gpsStream; -#endif - -bool radio_init(); -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(int8_t dbm); -void radio_reset_agc(); -mesh::LocalIdentity radio_new_identity(); \ No newline at end of file diff --git a/variants/lilygo_techo_card/variant.cpp b/variants/lilygo_techo_card/variant.cpp deleted file mode 100644 index 4cba928f..00000000 --- a/variants/lilygo_techo_card/variant.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // P0 -- pins 0 and 1 are hardwired for 32.768 kHz crystal (LFXO) - 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, - - // P1 - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h deleted file mode 100644 index e12453b3..00000000 --- a/variants/lilygo_techo_card/variant.h +++ /dev/null @@ -1,203 +0,0 @@ -/* - * variant.h -- LilyGo T-Echo Card pin definitions - * - * nRF52840 + SX1262 (HPB16B3) + SSD1315 OLED (72x40) + L76K GPS - * + MAX98357 Speaker + MP34DT05 PDM Mic + ICM20948 IMU + BQ25896 + Solar - * - * Cross-referenced against: - * - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h - * - Meshtastic PR #10267 (caveman99) - */ - -#pragma once - -#include "WVariant.h" - -//////////////////////////////////////////////////////////////////////////////// -// Low frequency clock source - -#define USE_LFXO // 32.768 kHz crystal -#define VARIANT_MCK (64000000ul) - -//////////////////////////////////////////////////////////////////////////////// -// Power / Battery - -#define PIN_VBAT_READ 2 // (0, 2) = AIN0 -#define BATTERY_ADC_AIN 0 // nRF SAADC AIN channel number - -// Gated voltage divider: drive HIGH before ADC read, LOW after -#define PIN_BAT_CTL 31 // (0, 31) -#define ADC_MULTIPLIER (2.0F) - -#define MV_LSB (3000.0F / 4096.0F) - -#define ADC_RESOLUTION (14) -#define BATTERY_SENSE_RES (12) -#define AREF_VOLTAGE (3.0) - -//////////////////////////////////////////////////////////////////////////////// -// Pin counts - -#define PINS_COUNT (48) -#define NUM_DIGITAL_PINS (48) -#define NUM_ANALOG_INPUTS (1) -#define NUM_ANALOG_OUTPUTS (0) - -//////////////////////////////////////////////////////////////////////////////// -// UART -- GPS (L76K) - -#define PIN_SERIAL1_RX 19 // (0, 19) -- GPS TX -> nRF RX -#define PIN_SERIAL1_TX 21 // (0, 21) -- nRF TX -> GPS RX - -//////////////////////////////////////////////////////////////////////////////// -// I2C (shared: OLED, IMU ICM20948) - -#define WIRE_INTERFACES_COUNT (1) -#define PIN_WIRE_SDA 36 // (1, 4) -#define PIN_WIRE_SCL 34 // (1, 2) - -//////////////////////////////////////////////////////////////////////////////// -// LEDs -- WS2812 addressable (no plain GPIO LED) -// The BSP drives LED_BUILTIN via digitalWrite for BLE status -- if pointed at -// the WS2812 data pin (39), it holds the line HIGH and all LEDs glow green. -// Point at an unused GPIO (46 = P1.14) so the BSP toggles harmlessly. - -#define LED_BUILTIN 46 // Unused GPIO -- keeps BSP happy -#define PIN_LED LED_BUILTIN -#define LED_RED LED_BUILTIN -#define LED_BLUE (-1) // Prevents Bluefruit flashing during advertising -#define PIN_STATUS_LED LED_BUILTIN -#define LED_STATE_ON 1 - -// WS2812 RGB LEDs -- 3 LEDs daisy-chained on a single data line (pin 39) -// Hardware verified: all three light when pin 39 is driven HIGH. -// Meshtastic PR #10267 mapped them as separate GPIOs (39, 44, 28) but -// testing confirms they're chained. -#define HAS_RGB_LED 1 -#define PIN_RGB_LED_1 39 // (1, 7) -- chain data in -#define PIN_NEOPIXEL PIN_RGB_LED_1 -#define NUM_NEOPIXELS 3 - -//////////////////////////////////////////////////////////////////////////////// -// Buttons - -#define PIN_BUTTON1 42 // (1, 10) -- orange front button -#define BUTTON_PIN PIN_BUTTON1 -#define PIN_USER_BTN BUTTON_PIN -// Boot button: P0.24 (hardware only, used for DFU) - -//////////////////////////////////////////////////////////////////////////////// -// SPI -- LoRa - -#define SPI_INTERFACES_COUNT (1) - -#define PIN_SPI_MISO 17 // (0, 17) -#define PIN_SPI_MOSI 15 // (0, 15) -#define PIN_SPI_SCK 13 // (0, 13) - -//////////////////////////////////////////////////////////////////////////////// -// SX1262 LoRa Radio (HPB16B3 / S62F module) - -#define USE_SX1262 -#define SX126X_CS 11 // (0, 11) -#define SX126X_DIO1 40 // (1, 8) -#define SX126X_BUSY 14 // (0, 14) -#define SX126X_RESET 7 // (0, 7) -#define SX126X_DIO2_AS_RF_SWITCH true -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 - -#define P_LORA_NSS SX126X_CS -#define P_LORA_DIO_1 SX126X_DIO1 -#define P_LORA_RESET SX126X_RESET -#define P_LORA_BUSY SX126X_BUSY -#define P_LORA_SCLK PIN_SPI_SCK -#define P_LORA_MISO PIN_SPI_MISO -#define P_LORA_MOSI PIN_SPI_MOSI - -// RF switch control lines (may be needed in addition to DIO2) -#define LORA_RF_VC1 27 // (0, 27) -#define LORA_RF_VC2 33 // (1, 1) - -//////////////////////////////////////////////////////////////////////////////// -// OLED Display -- SSD1315 (SSD1306-compatible), 72x40, I2C -// -// Physical panel is 72x40 within 128x64 GDDRAM. -// Visible window: columns 28–99, pages 3–7 (rows 24–63). -// SETDISPLAYOFFSET = 24 maps page 0 writes to the visible area. - -#define HAS_OLED 1 -#define OLED_I2C_ADDR 0x3C -#define OLED_WIDTH 72 -#define OLED_HEIGHT 40 -#define OLED_DISPLAY_OFFSET 24 - -// RT9080 enable -- controls 3V3 rail (OLED, GPS, LoRa, sensors) -#define PIN_OLED_EN 30 // (0, 30) -#define PIN_OLED_RESET (-1) - -//////////////////////////////////////////////////////////////////////////////// -// GPS -- L76K Multi-GNSS - -#define HAS_GPS 1 -#define GPS_EN_ACTIVE HIGH -#define GPS_BAUDRATE 9600 -#define PIN_GPS_TX 21 // nRF TX -> GPS RX (vendor GPS_UART_RX / P0.21) -#define PIN_GPS_RX 19 // nRF RX <- GPS TX (vendor GPS_UART_TX / P0.19) -#define PIN_GPS_EN 47 // (1, 15) -#define PIN_GPS_WAKEUP 25 // (0, 25) -#define PIN_GPS_1PPS 23 // (0, 23) -#define PIN_GPS_RF_EN 29 // (0, 29) - -//////////////////////////////////////////////////////////////////////////////// -// Speaker -- MAX98357 I2S Class-D Mono Amp - -#define HAS_SPEAKER 1 -#define PIN_SPK_EN 43 // (1, 11) -#define PIN_SPK_EN2 3 // (0, 3) -#define PIN_SPK_BCLK 16 // (0, 16) -#define PIN_SPK_DATA 20 // (0, 20) -#define PIN_SPK_LRCK 22 // (0, 22) - -//////////////////////////////////////////////////////////////////////////////// -// Microphone -- MP34DT05 Digital MEMS PDM - -#define HAS_MICROPHONE 1 -#define PIN_MIC_CLK 35 // (1, 3) -#define PIN_MIC_DATA 37 // (1, 5) - -//////////////////////////////////////////////////////////////////////////////// -// Buzzer - -#ifndef HAS_BUZZER -#define HAS_BUZZER 1 -#endif -#ifndef PIN_BUZZER -#define PIN_BUZZER 38 // (1, 6) -#endif - -//////////////////////////////////////////////////////////////////////////////// -// IMU -- ICM20948 - -#define HAS_IMU 1 -#define IMU_I2C_ADDR 0x68 - -//////////////////////////////////////////////////////////////////////////////// -// NFC -- nRF52840 NFC-A (dedicated P0.09/P0.10) - -#define HAS_NFC 1 - -//////////////////////////////////////////////////////////////////////////////// -// External Flash -- ZD25WQ32CEIGR 4MB QSPI - -#define HAS_EXT_FLASH 1 -#define PIN_QSPI_SCK 4 // (0, 4) -#define PIN_QSPI_CS 12 // (0, 12) -#define PIN_QSPI_IO0 6 // (0, 6) -#define PIN_QSPI_IO1 8 // (0, 8) -#define PIN_QSPI_IO2 41 // (1, 9) -#define PIN_QSPI_IO3 26 // (0, 26) - -//////////////////////////////////////////////////////////////////////////////// -// No dedicated RTC chip -- time from GPS or BLE companion sync - -#define HAS_RTC 0 \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/CPUPowerManager.h b/variants/lilygo_techo_lite_WIP/CPUPowerManager.h deleted file mode 100644 index 37d2bb90..00000000 --- a/variants/lilygo_techo_lite_WIP/CPUPowerManager.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -// CPUPowerManager.h — nRF52 no-op stub -// nRF52840 runs at fixed 64 MHz; no frequency scaling available. -// All methods are empty so main.cpp compiles without #ifdef guards. - -class CPUPowerManager { -public: - void begin() {} - void loop() {} - void setBoost() {} - void setIdle() {} - void setLowPower() {} - void clearLowPower() {} - int getFrequencyMHz() { return 64; } -}; diff --git a/variants/lilygo_techo_lite_WIP/FS.h b/variants/lilygo_techo_lite_WIP/FS.h deleted file mode 100644 index e4ce3242..00000000 --- a/variants/lilygo_techo_lite_WIP/FS.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -// FS.h — nRF52 compatibility stub -// ESP32 Arduino core provides this as the base filesystem abstraction. -// On nRF52, File and filesystem types come from Adafruit_LittleFS. -// This stub exists solely to satisfy #include in shared headers. - -#include -#include // struct tm, gmtime — implicit on ESP32, needs explicit on nRF52 - -// ESP32 FS.h defines these mode strings; some shared code references them -#ifndef FILE_READ -#define FILE_READ "r" -#endif -#ifndef FILE_WRITE -#define FILE_WRITE "w" -#endif -#ifndef FILE_APPEND -#define FILE_APPEND "a" -#endif \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/SD.h b/variants/lilygo_techo_lite_WIP/SD.h deleted file mode 100644 index 3b9e2773..00000000 --- a/variants/lilygo_techo_lite_WIP/SD.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -// SD.h — nRF52 compatibility stub for Meck -// Maps Arduino SD API to Adafruit InternalFS (LittleFS on QSPI flash). -// T-Echo Lite has no SD card slot; file operations use internal flash. - -#include -#include -#include // struct tm, gmtime — implicit on ESP32, explicit on nRF52 - -// ESP32 SD uses string file modes; define them here for compile compatibility -#ifndef FILE_READ -#define FILE_READ "r" -#endif -#ifndef FILE_WRITE -#define FILE_WRITE "w" -#endif - -class SDClass { -public: - // InternalFS is already initialised by main — begin() is a no-op - bool begin(uint8_t cs = 0) { return true; } - - // Accept any extra args (cs, SPI, freq) without complaint - template - bool begin(Args...) { return true; } - - bool exists(const char* path) { return InternalFS.exists(path); } - bool remove(const char* path) { return InternalFS.remove(path); } - bool mkdir(const char* path) { return InternalFS.mkdir(path); } - - // String mode overload — matches ESP32 SD API (FILE_READ="r", FILE_WRITE="w", "r+") - Adafruit_LittleFS_Namespace::File open(const char* path, const char* mode = "r") { - uint8_t m = FILE_O_READ; - if (mode) { - if (mode[0] == 'w') m = FILE_O_WRITE; - else if (mode[0] == 'r' && mode[1] == '+') m = FILE_O_WRITE; - } - return InternalFS.open(path, m); - } -}; - -// Static instance per translation unit — no state (just forwards to InternalFS singleton) -static SDClass SD; \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/TechoBoard.cpp b/variants/lilygo_techo_lite_WIP/TechoBoard.cpp deleted file mode 100644 index a11d31b2..00000000 --- a/variants/lilygo_techo_lite_WIP/TechoBoard.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include - -#include "TechoBoard.h" - -#ifdef LILYGO_TECHO - -void TechoBoard::begin() { - NRF52Board::begin(); - - // Configure battery measurement control BEFORE Wire.begin() - // to ensure P0.02 is not claimed by another peripheral - pinMode(PIN_VBAT_MEAS_EN, OUTPUT); - digitalWrite(PIN_VBAT_MEAS_EN, LOW); - pinMode(PIN_VBAT_READ, INPUT); - - Wire.begin(); - - pinMode(SX126X_POWER_EN, OUTPUT); - digitalWrite(SX126X_POWER_EN, HIGH); - delay(10); -} - -uint16_t TechoBoard::getBattMilliVolts() { - // Use LilyGo's exact ADC configuration - analogReference(AR_INTERNAL_3_0); - analogReadResolution(12); - - // Enable battery voltage divider (MOSFET gate on P0.31) - pinMode(PIN_VBAT_MEAS_EN, OUTPUT); - digitalWrite(PIN_VBAT_MEAS_EN, HIGH); - - // Reclaim P0.02 for analog input (in case another peripheral touched it) - pinMode(PIN_VBAT_READ, INPUT); - delay(10); // let divider + ADC settle - - // Read and average (matching LilyGo's approach) - uint32_t sum = 0; - for (int i = 0; i < 8; i++) { - sum += analogRead(PIN_VBAT_READ); - delayMicroseconds(100); - } - uint16_t adc = sum / 8; - - // Disable divider to save power - digitalWrite(PIN_VBAT_MEAS_EN, LOW); - - // LilyGo's exact formula: adc * (3000.0 / 4096.0) * 2.0 - // = adc * 0.73242188 * 2.0 = adc * 1.46484375 - uint16_t millivolts = (uint16_t)((float)adc * (3000.0f / 4096.0f) * 2.0f); - - return millivolts; -} -#endif \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/TechoBoard.h b/variants/lilygo_techo_lite_WIP/TechoBoard.h deleted file mode 100644 index 1bf00936..00000000 --- a/variants/lilygo_techo_lite_WIP/TechoBoard.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include -#include - -// ============================================================ -// T-Echo Lite battery pins — hardcoded from LilyGo t_echo_lite_config.h -// NOT using any defines from variant.h for battery measurement -// ============================================================ -#define PIN_VBAT_READ _PINNUM(0, 2) // BATTERY_ADC_DATA -#define PIN_VBAT_MEAS_EN _PINNUM(0, 31) // BATTERY_MEASUREMENT_CONTROL - -class TechoBoard : public NRF52BoardDCDC { -public: - TechoBoard() {} - void begin(); - uint16_t getBattMilliVolts() override; - - const char* getManufacturerName() const override { - return "LilyGo T-Echo Lite"; - } - - void powerOff() override { - digitalWrite(PIN_VBAT_MEAS_EN, LOW); - #ifdef LED_RED - digitalWrite(LED_RED, LOW); - #endif - #ifdef LED_GREEN - digitalWrite(LED_GREEN, LOW); - #endif - #ifdef LED_BLUE - digitalWrite(LED_BLUE, LOW); - #endif - #ifdef DISP_BACKLIGHT - digitalWrite(DISP_BACKLIGHT, LOW); - #endif - #ifdef PIN_PWR_EN - digitalWrite(PIN_PWR_EN, LOW); - #endif - sd_power_system_off(); - } -}; \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/platformio.ini b/variants/lilygo_techo_lite_WIP/platformio.ini deleted file mode 100644 index 73e9e35b..00000000 --- a/variants/lilygo_techo_lite_WIP/platformio.ini +++ /dev/null @@ -1,268 +0,0 @@ -; ============================================================================= -; LilyGo T-Echo Lite — Meck variant configuration -; -; nRF52840 + SX1262 + GxEPD2 1.22" e-ink (176×192, GDEM0122T61/SSD1681) -; + CardKB via QWIIC (0x5F) + optional L76K GPS -; -; Display: GxEPD2_122_T61 — full refresh only (~2s), no fast/partial refresh. -; UI must minimise unnecessary redraws. -; Scale factors: 1.5×/2.0× give ~117×96 virtual coordinate space. -; -; Platform: nRF52 (Adafruit nRF52 Arduino) -; Board JSON: boards/t-echo.json (nRF52840 PCA10056 compatible) -; ============================================================================= - -; --- Base configuration for all T-Echo Lite Meck builds (with display) --- -[lilygo_techo_lite_meck] -extends = nrf52_base -board = t-echo -board_build.ldscript = boards/nrf52840_s140_v6.ld -extra_scripts = pre:patch_nrf52_bsp.py -build_flags = ${nrf52_base.build_flags} - -I variants/lilygo_techo_lite - -I src/helpers/nrf52 - -I lib/nrf52/s140_nrf52_6.1.1_API/include - -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 - -D LILYGO_TECHO - -D LILYGO_TECHO_LITE - -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 - -D SX126X_POWER_EN=30 - -D SX126X_CURRENT_LIMIT=140 - -D SX126X_RX_BOOSTED_GAIN=1 - ; nRF52 compatibility — no PSRAM, no SD card, fallback GPS baud for CLI code paths - -D ps_calloc=calloc - -D ps_malloc=malloc - -D GPS_BAUDRATE=9600 - -D SDCARD_CS=-1 - -D ROW_AUTO_LOCK=255 - ; Display — GxEPD2 1.22" e-ink (176×192, SSD1681) - -D DISPLAY_CLASS=GxEPDDisplay - -D EINK_DISPLAY_MODEL=GxEPD2_122_T61 - -D EINK_SCALE_X=1.5f - -D EINK_SCALE_Y=2.0f - -D EINK_X_OFFSET=6 - -D EINK_Y_OFFSET=1 - -D DISPLAY_ROTATION=4 - -D EINK_FULL_REFRESH_ONLY=1 - -D EINK_VIRTUAL_W=117 - -D EINK_VIRTUAL_H=88 - -D AUTO_OFF_MILLIS=0 -build_src_filter = ${nrf52_base.build_src_filter} - + - + - + - + - + - +<../variants/lilygo_techo_lite> -lib_deps = - ${nrf52_base.lib_deps} - stevemarple/MicroNMEA @ ^2.0.6 - adafruit/Adafruit BME280 Library @ ^2.3.0 - https://github.com/SoulOfNoob/GxEPD2.git - bakercp/CRC32 @ ^2.0.0 -debug_tool = jlink -upload_protocol = nrfutil - -; ============================================================================= -; Build Environments -; ============================================================================= - -; --- BLE Companion Radio (no GPS) --- -; Pairs with MeshCore companion app over Bluetooth. -; CardKB provides on-device text input for standalone messaging. -; No GPS — time synced via BLE companion or serial CLI. -[env:meck_techo_lite_ble] -extends = lilygo_techo_lite_meck -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${lilygo_techo_lite_meck.build_flags} - -I src/helpers/ui - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=250 - -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=20 - -D BLE_PIN_CODE=123456 - ; -D BLE_DEBUG_LOGGING=1 - -D OFFLINE_QUEUE_SIZE=64 - -D MECK_CARDKB - -D UI_SENSORS_PAGE=1 - ; -D MESH_PACKET_LOGGING=1 - ; -D MESH_DEBUG=1 - -D AUTO_SHUTDOWN_MILLIVOLTS=2800 -build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${lilygo_techo_lite_meck.lib_deps} - densaugeo/base64 @ ~1.4.0 - -; --- Standalone Radio (no BLE, CardKB + display only) --- -; No companion app — device IS the terminal. -; USB serial for CLI configuration. -; Frees ~20-30KB BLE RAM → room for 500 contacts + larger message history. -[env:meck_techo_lite_standalone] -extends = lilygo_techo_lite_meck -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${lilygo_techo_lite_meck.build_flags} - -I src/helpers/ui - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 - -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=150 - -D OFFLINE_QUEUE_SIZE=1 - -D MECK_CARDKB - -D UI_SENSORS_PAGE=1 - -D AUTO_SHUTDOWN_MILLIVOLTS=2800 -build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${lilygo_techo_lite_meck.lib_deps} - densaugeo/base64 @ ~1.4.0 - -; --- BLE Companion Radio (with GPS) --- -; Same as above + L76K GPS for location and time sync. -; Requires external GPS module connected to UART1 (TX=P0.29, RX=P1.10). -[env:meck_techo_lite_gps_ble] -extends = lilygo_techo_lite_meck -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${lilygo_techo_lite_meck.build_flags} - -I src/helpers/ui - -I examples/companion_radio/ui-new - -D ENV_INCLUDE_GPS=1 - -D GPS_BAUD_RATE=9600 - -D PIN_GPS_EN=GPS_EN - -D MAX_CONTACTS=250 - -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=20 - -D BLE_PIN_CODE=123456 - -D OFFLINE_QUEUE_SIZE=64 - -D MECK_CARDKB - -D UI_RECENT_LIST_SIZE=9 - -D UI_SENSORS_PAGE=1 - -D AUTO_SHUTDOWN_MILLIVOLTS=2800 -build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${lilygo_techo_lite_meck.lib_deps} - stevemarple/MicroNMEA @ ^2.0.6 - densaugeo/base64 @ ~1.4.0 - -; --- Repeater --- -; Standalone LoRa repeater node. E-ink shows status. -; CardKB not useful here but included by base for consistency. -[env:meck_techo_lite_repeater] -extends = lilygo_techo_lite_meck -build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} - +<../examples/simple_repeater> -build_flags = - ${lilygo_techo_lite_meck.build_flags} - -D ADVERT_NAME='"Meck T-Echo Lite Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=50 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 - -; --- Room Server --- -; BBS-style message board node. -[env:meck_techo_lite_room_server] -extends = lilygo_techo_lite_meck -build_src_filter = ${lilygo_techo_lite_meck.build_src_filter} - +<../examples/simple_room_server> -build_flags = - ${lilygo_techo_lite_meck.build_flags} - -D ADVERT_NAME='"Meck T-Echo Lite Room"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 - -; ============================================================================= -; Headless (no display) variants -; ============================================================================= - -; --- Headless base (no display, no GxEPD2) --- -[lilygo_techo_lite_meck_core] -extends = nrf52_base -board = t-echo -board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} - -I variants/lilygo_techo_lite - -I src/helpers/nrf52 - -I lib/nrf52/s140_nrf52_6.1.1_API/include - -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 - -D LILYGO_TECHO - -D LILYGO_TECHO_LITE - -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 - -D SX126X_POWER_EN=30 - -D SX126X_CURRENT_LIMIT=140 - -D SX126X_RX_BOOSTED_GAIN=1 - ; nRF52 compatibility - -D ps_calloc=calloc - -D ps_malloc=malloc - -D GPS_BAUDRATE=9600 - -D SDCARD_CS=-1 - -D ROW_AUTO_LOCK=255 - -D DISABLE_DIAGNOSTIC_OUTPUT - -D AUTO_OFF_MILLIS=0 -build_src_filter = ${nrf52_base.build_src_filter} - + - + - + - + - +<../variants/lilygo_techo_lite> -lib_deps = - ${nrf52_base.lib_deps} - stevemarple/MicroNMEA @ ^2.0.6 - adafruit/Adafruit BME280 Library @ ^2.3.0 - bakercp/CRC32 @ ^2.0.0 -debug_tool = jlink -upload_protocol = nrfutil - -; --- Headless Repeater (no display — lowest power, outdoor deployment) --- -[env:meck_techo_lite_core_repeater] -extends = lilygo_techo_lite_meck_core -build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter} - +<../examples/simple_repeater> -build_flags = - ${lilygo_techo_lite_meck_core.build_flags} - -D ADVERT_NAME='"Meck T-Echo Lite Core Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=50 - -; --- Headless BLE Companion (no display — phone-only UI) --- -[env:meck_techo_lite_core_ble] -extends = lilygo_techo_lite_meck_core -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${lilygo_techo_lite_meck_core.build_flags} - -D MAX_CONTACTS=250 - -D MAX_GROUP_CHANNELS=8 - -D CHANNEL_MSG_HISTORY_SIZE=20 - -D BLE_PIN_CODE=234567 - -D OFFLINE_QUEUE_SIZE=64 - -D AUTO_SHUTDOWN_MILLIVOLTS=2800 -build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter} - + - +<../examples/companion_radio/*.cpp> -lib_deps = - ${lilygo_techo_lite_meck_core.lib_deps} - densaugeo/base64 @ ~1.4.0 \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/target.cpp b/variants/lilygo_techo_lite_WIP/target.cpp deleted file mode 100644 index 5d18ba38..00000000 --- a/variants/lilygo_techo_lite_WIP/target.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include "target.h" -#include -#include - -TechoBoard board; - -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); - -WRAPPER_CLASS radio_driver(radio, board); - -VolatileRTCClock fallback_clock; -AutoDiscoverRTCClock rtc_clock(fallback_clock); - -#ifdef ENV_INCLUDE_GPS -MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); -EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); -#else -EnvironmentSensorManager sensors = EnvironmentSensorManager(); -#endif - -#ifdef DISPLAY_CLASS - DISPLAY_CLASS display; - MomentaryButton user_btn(PIN_USER_BTN, 1000, true); -#endif - -bool radio_init() { - rtc_clock.begin(Wire); - - return radio.std_init(&SPI); -} - -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(int8_t dbm) { - radio.setOutputPower(dbm); -} - -mesh::LocalIdentity radio_new_identity() { - RadioNoiseListener rng(radio); - return mesh::LocalIdentity(&rng); // create new random identity -} - -void radio_reset_agc() { - radio.setRxBoostedGainMode(true); -} \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/target.h b/variants/lilygo_techo_lite_WIP/target.h deleted file mode 100644 index 02c83805..00000000 --- a/variants/lilygo_techo_lite_WIP/target.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#define RADIOLIB_STATIC_ONLY 1 -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef DISPLAY_CLASS - #include - #include -#endif - -extern TechoBoard board; -extern WRAPPER_CLASS radio_driver; -extern AutoDiscoverRTCClock rtc_clock; -extern EnvironmentSensorManager sensors; - -#ifdef DISPLAY_CLASS - extern DISPLAY_CLASS display; - extern MomentaryButton user_btn; -#endif - -bool radio_init(); -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(int8_t dbm); -mesh::LocalIdentity radio_new_identity(); -void radio_reset_agc(); \ No newline at end of file diff --git a/variants/lilygo_techo_lite_WIP/variant.cpp b/variants/lilygo_techo_lite_WIP/variant.cpp deleted file mode 100644 index 3cd82d70..00000000 --- a/variants/lilygo_techo_lite_WIP/variant.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "variant.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const int MISO = PIN_SPI1_MISO; -const int MOSI = PIN_SPI1_MOSI; -const int SCK = PIN_SPI1_SCK; - -const uint32_t g_ADigitalPinMap[] = { - 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, - 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47 -}; - -void initVariant() { - pinMode(PIN_PWR_EN, OUTPUT); - digitalWrite(PIN_PWR_EN, HIGH); - - pinMode(PIN_BUTTON1, INPUT_PULLUP); - pinMode(PIN_BUTTON2, INPUT_PULLUP); - - pinMode(LED_RED, OUTPUT); - pinMode(LED_GREEN, OUTPUT); - pinMode(LED_BLUE, OUTPUT); - digitalWrite(LED_BLUE, HIGH); - digitalWrite(LED_GREEN, HIGH); - digitalWrite(LED_RED, HIGH); - - // pinMode(PIN_TXCO, OUTPUT); - // digitalWrite(PIN_TXCO, HIGH); - - pinMode(DISP_POWER, OUTPUT); - digitalWrite(DISP_POWER, LOW); - - // shutdown gps - pinMode(GPS_EN, OUTPUT); - digitalWrite(GPS_EN, LOW); -} diff --git a/variants/lilygo_techo_lite_WIP/variant.h b/variants/lilygo_techo_lite_WIP/variant.h deleted file mode 100644 index 07202165..00000000 --- a/variants/lilygo_techo_lite_WIP/variant.h +++ /dev/null @@ -1,159 +0,0 @@ -/* - * variant.h - * Copyright (C) 2023 Seeed K.K. - * MIT License - */ - -#pragma once - -#define _PINNUM(port, pin) ((port) * 32 + (pin)) - -#include "WVariant.h" - -//////////////////////////////////////////////////////////////////////////////// -// Low frequency clock source - -#define USE_LFXO // 32.768 kHz crystal oscillator -#define VARIANT_MCK (64000000ul) - -#define WIRE_INTERFACES_COUNT (1) - -//////////////////////////////////////////////////////////////////////////////// -// Power - -#define PIN_PWR_EN _PINNUM(0, 30) // RT9080_EN - -#define BATTERY_PIN _PINNUM(0, 2) -#define ADC_MULTIPLIER (2.0F) - -#define ADC_RESOLUTION (14) -#define BATTERY_SENSE_RES (12) - -#define AREF_VOLTAGE (3.0) - -//////////////////////////////////////////////////////////////////////////////// -// Number of pins - -#define PINS_COUNT (48) -#define NUM_DIGITAL_PINS (48) -#define NUM_ANALOG_INPUTS (1) -#define NUM_ANALOG_OUTPUTS (0) - -//////////////////////////////////////////////////////////////////////////////// -// UART pin definition - -#define PIN_SERIAL1_RX PIN_GPS_TX -#define PIN_SERIAL1_TX PIN_GPS_RX -//////////////////////////////////////////////////////////////////////////////// -// I2C pin definition - -#define PIN_WIRE_SDA _PINNUM(1, 4) // (SDA) - per LilyGo IIC_1_SDA -#define PIN_WIRE_SCL _PINNUM(1, 2) // (SCL) - per LilyGo IIC_1_SCL - -//////////////////////////////////////////////////////////////////////////////// -// SPI pin definition - -#define SPI_INTERFACES_COUNT (2) - -#define PIN_SPI_MISO _PINNUM(0, 17) // (MISO) -#define PIN_SPI_MOSI _PINNUM(0, 15) // (MOSI) -#define PIN_SPI_SCK _PINNUM(0, 13) // (SCK) -#define PIN_SPI_NSS (-1) - -//////////////////////////////////////////////////////////////////////////////// -// QSPI FLASH - -#define PIN_QSPI_SCK _PINNUM(0, 4) -#define PIN_QSPI_CS _PINNUM(0, 12) -#define PIN_QSPI_IO0 _PINNUM(0, 6) -#define PIN_QSPI_IO1 _PINNUM(0, 8) -#define PIN_QSPI_IO2 _PINNUM(1, 9) -#define PIN_QSPI_IO3 _PINNUM(0, 26) - -#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR -#define EXTERNAL_FLASH_USE_QSPI - -//////////////////////////////////////////////////////////////////////////////// -// Builtin LEDs - -#define LED_RED _PINNUM(1, 14) // LED_3 -#define LED_BLUE _PINNUM(1, 5) // LED_2 -#define LED_GREEN _PINNUM(1, 7) // LED_1 - -//#define PIN_STATUS_LED LED_BLUE -#define LED_BUILTIN (-1) -#define LED_PIN LED_BUILTIN -#define LED_STATE_ON LOW - -//////////////////////////////////////////////////////////////////////////////// -// Builtin buttons - -#define PIN_BUTTON1 _PINNUM(0, 24) // BOOT -#define BUTTON_PIN PIN_BUTTON1 -#define PIN_USER_BTN BUTTON_PIN - -#define PIN_BUTTON2 _PINNUM(0, 18) -#define BUTTON_PIN2 PIN_BUTTON2 - -#define EXTERNAL_FLASH_DEVICES MX25R1635F -#define EXTERNAL_FLASH_USE_QSPI - -//////////////////////////////////////////////////////////////////////////////// -// Lora - -#define USE_SX1262 -#define LORA_CS _PINNUM(0, 11) -#define SX126X_POWER_EN _PINNUM(0, 30) -#define SX126X_DIO1 _PINNUM(1, 8) -#define SX126X_BUSY _PINNUM(0, 14) -#define SX126X_RESET _PINNUM(0, 7) -#define SX126X_RF_VC1 _PINNUM(0, 27) -#define SX126X_RF_VC2 _PINNUM(0, 33) - -#define P_LORA_DIO_1 SX126X_DIO1 -#define P_LORA_NSS LORA_CS -#define P_LORA_RESET SX126X_RESET -#define P_LORA_BUSY SX126X_BUSY -#define P_LORA_SCLK PIN_SPI_SCK -#define P_LORA_MISO PIN_SPI_MISO -#define P_LORA_MOSI PIN_SPI_MOSI - -//////////////////////////////////////////////////////////////////////////////// -// SPI1 - -#define PIN_SPI1_MISO (-1) // Not used for Display -#define PIN_SPI1_MOSI _PINNUM(0, 20) -#define PIN_SPI1_SCK _PINNUM(0, 19) - -// GxEPD2 needs that for a panel that is not even used ! -extern const int MISO; -extern const int MOSI; -extern const int SCK; - -//////////////////////////////////////////////////////////////////////////////// -// Display - -// #define DISP_MISO (-1) // Not used for Display -#define DISP_MOSI _PINNUM(0, 20) -#define DISP_SCLK _PINNUM(0, 19) -#define DISP_CS _PINNUM(0, 22) -#define DISP_DC _PINNUM(0, 21) -#define DISP_RST _PINNUM(0, 28) -#define DISP_BUSY _PINNUM(0, 3) -#define DISP_POWER _PINNUM(1, 12) -// #define DISP_BACKLIGHT (-1) // Display has no backlight - -#define PIN_DISPLAY_CS DISP_CS -#define PIN_DISPLAY_DC DISP_DC -#define PIN_DISPLAY_RST DISP_RST -#define PIN_DISPLAY_BUSY DISP_BUSY - -//////////////////////////////////////////////////////////////////////////////// -// GPS — per LilyGo t_echo_lite_config.h -// PIN_GPS_TX/RX named from GPS module's perspective - -#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX -#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX -#define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable -#define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up -#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS \ No newline at end of file