mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-06 08:42:48 +02:00
Added new emoji to emoji picker; bug fixes for larger battery capacities recognition regression; updated readme for clarity on contacts add; updated firmware version; fix stupid blob storage size & buffer issue so now you can add contacts from last heard finally!!! Updated readme accordingly
This commit is contained in:
@@ -274,18 +274,73 @@ The GPS page also shows the current time, satellite count, position, altitude, a
|
||||
|
||||
### Contacts Screen
|
||||
|
||||
Press **C** from the home screen to open the contacts list. All known mesh contacts are shown sorted by most recently seen, with their type (Chat, Repeater, Room, Sensor), hop count, and time since last advert.
|
||||
Press **C** from the home screen to open the contacts list. All known mesh contacts are shown sorted by most recently heard, with their type prefix, estimated hop count, and time since last advert.
|
||||
|
||||
**Contact type prefixes**
|
||||
|
||||
| Prefix | Type |
|
||||
|--------|------|
|
||||
| C | Chat node |
|
||||
| R | Repeater |
|
||||
| RS | Room server |
|
||||
| ? | Unknown / sensor |
|
||||
|
||||
**Hop count display**
|
||||
|
||||
| Display | Meaning |
|
||||
|---------|---------|
|
||||
| `D` | Direct path known (path exchange completed) |
|
||||
| `D*` | Direct path, manually locked |
|
||||
| `N` | N-hop path known (e.g. `2` = 2 hops) |
|
||||
| `N*` | N-hop path, manually locked |
|
||||
| `~D` | Heard direct via flood advert (no path exchange yet) |
|
||||
| `~N` | Estimated N hops via flood advert |
|
||||
| `?` | No path information available |
|
||||
|
||||
Flood-based hop estimates (`~D`, `~N`) are shown for the 12 most recently heard contacts and reset to `?` on reboot until each contact re-advertises. Confirmed path values (`D`, `N`) persist until overwritten by a new path exchange.
|
||||
|
||||
**Normal mode controls**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| W / S | Scroll up / down through contacts |
|
||||
| A / D | Cycle filter: All → Chat → Repeater → Room → Sensor → Favourites |
|
||||
| Enter | Open DM compose (Chat contact) or repeater admin (Repeater contact) |
|
||||
| X | Export contacts to SD card (wait 5–10 seconds for confirmation popup) |
|
||||
| R | Import contacts from SD card (wait 5–10 seconds for confirmation popup) |
|
||||
| A / D | Cycle filter: All → Chat → Rptr → Room → Sens → Fav |
|
||||
| Enter | Enter select mode (highlights current contact, enables batch operations) |
|
||||
| P | Open path editor for the highlighted contact |
|
||||
| Q | Back to home screen |
|
||||
|
||||
**Contact limits:** Standalone and WiFi variants support up to 1,500 contacts (stored in PSRAM). BLE variants (Audio-BLE and 4G-BLE) are limited to 500 contacts due to BLE protocol constraints.
|
||||
> **Note:** The **Fav** filter shows only contacts you have marked as favourites. If it appears empty, no contacts have been favourited yet — use select mode (Enter) and then **F** to mark contacts.
|
||||
|
||||
**Select mode** — press Enter from the contacts list to enter select mode. The highlighted contact is pre-selected. Use W/S to scroll and Enter to toggle selection on any row.
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| W / S | Scroll up / down |
|
||||
| Enter | Toggle selection on current contact |
|
||||
| A | Select all contacts in current filter |
|
||||
| D | Deselect all |
|
||||
| F | Toggle favourite on all selected contacts |
|
||||
| X | Export selected contacts to SD card |
|
||||
| Backspace | Delete selected contacts |
|
||||
| Q | Exit select mode |
|
||||
|
||||
**Adding contacts**
|
||||
|
||||
Contacts can be added three ways:
|
||||
|
||||
1. **Automatic** — if Settings → Contacts → Add Mode is set to *Auto All*, any node whose advert is heard is added automatically. *Custom* mode adds only nodes matching the enabled type toggles (Companion, Repeater, Room Server, Sensor) — each toggle controls whether receiving an advert of that type triggers an auto-add. *Manual Only* disables all auto-add.
|
||||
|
||||
2. **From the Last Heard screen** — press **H** from the home screen to open the last-heard advert list. Scroll to the node you want and press **Enter** (or tap the row) to add it to contacts. Press **Enter** again on an existing contact to remove it (favourites require a second press within 3 seconds to confirm). Entries show `[+]` if already in contacts, `[★]` if a favourite.
|
||||
|
||||
> **Note:** The Last Heard list holds up to 1,000 entries in PSRAM, and advert data is stored persistently on the SD card — so contacts can be added long after the original advertisement was received, even across reboots. This makes Last Heard especially useful when auto-add is set to *Manual Only*, as it provides a passive catalogue of every node heard on the network.
|
||||
|
||||
3. **From the Discovery screen** — press **F** from the home screen to run an active discovery scan. Nodes that respond appear in a list; press **Enter** on any entry to add it to contacts.
|
||||
|
||||
**Deleting contacts**
|
||||
|
||||
Enter select mode (Enter), select the contacts to remove (Enter to toggle, A to select all), then press **Backspace** to delete. You will be returned to the contacts list once the deletion is complete.
|
||||
|
||||
**Contact limits:** Standalone and WiFi variants support up to 1,500 contacts (stored in PSRAM). BLE variants are limited to 500 contacts due to BLE protocol constraints.
|
||||
|
||||
### Sending a Direct Message
|
||||
|
||||
@@ -358,7 +413,17 @@ Press **S** from the home screen to open settings. On first boot (when the devic
|
||||
| Channels >> | Opens the Channels sub-screen (see below) |
|
||||
| Device Info | Public key and firmware version (read-only) |
|
||||
|
||||
**Contacts sub-screen** — press Enter on the `Contacts >>` row to open. Contains the contact auto-add mode picker (Auto All / Custom / Manual) and, when set to Custom, per-type toggles for Chat, Repeater, Room Server, Sensor, and an Overwrite Oldest option. Press Q to return to the top-level settings list.
|
||||
**Contacts sub-screen** — press Enter on the `Contacts >>` row to open. Contains the contact auto-add mode picker and, when set to Custom, per-type toggles:
|
||||
|
||||
| Toggle | Meaning when ON |
|
||||
|--------|----------------|
|
||||
| Companion | Auto-add a chat node when its advert is heard |
|
||||
| Repeater | Auto-add repeaters heard via advert |
|
||||
| Room Server | Auto-add room servers heard via advert |
|
||||
| Sensor | Auto-add sensor nodes heard via advert |
|
||||
| Overwrite Oldest | When the contact list is full, overwrite the oldest non-favourite entry instead of discarding the new contact |
|
||||
|
||||
Press Q to return to the top-level settings list.
|
||||
|
||||
**Channels sub-screen** — press Enter on the `Channels >>` row to open. Lists all current channels, with an option to add hashtag channels or delete non-primary channels (X). Press Q to return to the top-level settings list.
|
||||
|
||||
@@ -631,13 +696,44 @@ The UTC offset is configured in the Settings screen (same as T-Deck Pro) and is
|
||||
|
||||
#### Contacts
|
||||
|
||||
The contacts list shows all known nodes sorted by most recently heard, with type prefix, estimated hop count, and time since last advert. See the [T-Deck Pro Contacts Screen](#contacts-screen) section for an explanation of the type prefix and hop count display — the same conventions apply on the T5S3.
|
||||
|
||||
**Normal mode**
|
||||
|
||||
| Gesture | Action |
|
||||
|---------|--------|
|
||||
| Swipe up / down | Scroll through contacts |
|
||||
| Swipe left / right | Cycle contact filter (All → Chat → Repeater → Room → Sensor → Favourites) |
|
||||
| Tap | Select contact |
|
||||
| Swipe left / right | Cycle contact filter (All → Chat → Rptr → Room → Sens → Fav) |
|
||||
| Tap | Enter select mode (tapped contact is pre-selected) |
|
||||
| Long press on Chat contact | View unread DMs (if any), then compose DM |
|
||||
| Long press on Repeater contact | Open repeater admin login |
|
||||
| Long press on Repeater/RS contact | Open repeater admin login |
|
||||
|
||||
> **Note:** The **Fav** filter shows only contacts you have marked as favourites. If it appears empty, no contacts have been favourited yet — use select mode (tap a contact) and then long-press to mark favourites.
|
||||
|
||||
**Select mode** — tap any contact row to enter select mode. The tapped contact is pre-selected (shown with `*`). Swipe or tap to navigate and toggle selections.
|
||||
|
||||
| Gesture | Action |
|
||||
|---------|--------|
|
||||
| Tap | Toggle selection on tapped row |
|
||||
| Swipe left | Select all contacts in current filter |
|
||||
| Swipe right | Deselect all |
|
||||
| Long press | Exit select mode (confirm favourites / deletions first) |
|
||||
|
||||
Batch operations (favourite toggle, delete) are triggered from the overlay that appears after exiting select mode with contacts selected.
|
||||
|
||||
**Adding contacts**
|
||||
|
||||
1. **Automatic** — if Settings → Contacts → Add Mode is set to *Auto All*, nodes are added as their adverts are heard. *Custom* mode adds only nodes matching the enabled type toggles (Companion, Repeater, Room Server, Sensor). *Manual Only* disables auto-add.
|
||||
|
||||
2. **From the Last Heard screen** — tap the **Discover** tile (or access via the home page on non-WiFi builds) and navigate to Last Heard. Tap any entry to add it to contacts, or tap an existing contact to remove it (favourites require a second tap within 3 seconds to confirm). Entries show `[+]` if already in contacts, `[★]` if a favourite.
|
||||
|
||||
> **Note:** The Last Heard list holds up to 1,000 entries in PSRAM, and advert data is stored persistently on the SD card — so contacts can be added long after the original advertisement was received, even across reboots. This makes Last Heard especially useful when auto-add is set to *Manual Only*, as it provides a passive catalogue of every node heard on the network.
|
||||
|
||||
3. **From the Discovery screen** — tap the Discover tile and run an active scan. Tap any result to add it to contacts.
|
||||
|
||||
**Deleting contacts**
|
||||
|
||||
Tap a contact to enter select mode, select the contacts to remove, exit select mode, and choose delete from the confirmation overlay.
|
||||
|
||||
#### Text Reader (File List)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "DataStore.h"
|
||||
|
||||
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
||||
#define MAX_BLOBRECS 100
|
||||
#define MAX_BLOBRECS 1000
|
||||
#else
|
||||
#define MAX_BLOBRECS 20
|
||||
#endif
|
||||
@@ -691,16 +691,27 @@ struct BlobRec {
|
||||
};
|
||||
|
||||
void DataStore::checkAdvBlobFile() {
|
||||
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
|
||||
File file = openWrite(_getContactsChannelsFS(), "/adv_blobs");
|
||||
if (file) {
|
||||
BlobRec zeroes;
|
||||
memset(&zeroes, 0, sizeof(zeroes));
|
||||
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
|
||||
file.write((uint8_t *) &zeroes, sizeof(zeroes));
|
||||
}
|
||||
file.close();
|
||||
FILESYSTEM* fs = _getContactsChannelsFS();
|
||||
size_t expectedSize = (size_t)MAX_BLOBRECS * sizeof(BlobRec);
|
||||
|
||||
if (fs->exists("/adv_blobs")) {
|
||||
File existing = openRead(fs, "/adv_blobs");
|
||||
size_t actualSize = existing ? (size_t)existing.size() : 0;
|
||||
if (existing) existing.close();
|
||||
if (actualSize == expectedSize) return; // already correct size
|
||||
Serial.printf("[DataStore] adv_blobs wrong size (%u vs %u) — recreating\n",
|
||||
(unsigned)actualSize, (unsigned)expectedSize);
|
||||
fs->remove("/adv_blobs"); // delete undersized (or oversized) file
|
||||
}
|
||||
|
||||
File file = openWrite(fs, "/adv_blobs");
|
||||
if (file) {
|
||||
BlobRec zeroes;
|
||||
memset(&zeroes, 0, sizeof(zeroes));
|
||||
for (int i = 0; i < MAX_BLOBRECS; i++) {
|
||||
file.write((uint8_t *) &zeroes, sizeof(zeroes));
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,10 +884,14 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b
|
||||
|
||||
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
|
||||
mesh::Utils::toHex(fname, key, key_len);
|
||||
// Prefer SD card (_fsExtra) — unlimited file count vs SPIFFS ~100-200 file limit.
|
||||
// Fall back to SPIFFS (_fs) for devices without SD.
|
||||
FILESYSTEM* blobFs = (_fsExtra != nullptr) ? _fsExtra : _fs;
|
||||
|
||||
sprintf(path, "/bl/%s", fname);
|
||||
|
||||
if (_fs->exists(path)) {
|
||||
File f = openRead(_fs, path);
|
||||
if (blobFs->exists(path)) {
|
||||
File f = openRead(blobFs, path);
|
||||
if (f) {
|
||||
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
|
||||
f.close();
|
||||
@@ -892,15 +907,20 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
|
||||
|
||||
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
|
||||
mesh::Utils::toHex(fname, key, key_len);
|
||||
// Prefer SD card (_fsExtra) — unlimited file count vs SPIFFS ~100-200 file limit.
|
||||
FILESYSTEM* blobFs = (_fsExtra != nullptr) ? _fsExtra : _fs;
|
||||
|
||||
blobFs->mkdir("/bl"); // ensure directory exists on chosen filesystem
|
||||
|
||||
sprintf(path, "/bl/%s", fname);
|
||||
|
||||
File f = openWrite(_fs, path);
|
||||
File f = openWrite(blobFs, path);
|
||||
if (f) {
|
||||
int n = f.write(src_buf, len);
|
||||
f.close();
|
||||
if (n == len) return true; // success!
|
||||
|
||||
_fs->remove(path); // blob was only partially written!
|
||||
blobFs->remove(path); // blob was only partially written!
|
||||
}
|
||||
return false; // error
|
||||
}
|
||||
|
||||
@@ -1265,7 +1265,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
next_ack_idx = 0;
|
||||
sign_data = NULL;
|
||||
dirty_contacts_expiry = 0;
|
||||
memset(advert_paths, 0, sizeof(advert_paths));
|
||||
advert_paths = nullptr; // PSRAM-allocated in begin()
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key));
|
||||
memset(_sent_track, 0, sizeof(_sent_track));
|
||||
_sent_track_idx = 0;
|
||||
@@ -1292,6 +1292,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
}
|
||||
|
||||
void MyMesh::begin(bool has_display) {
|
||||
advert_paths = (AdvertPath*)ps_calloc(ADVERT_PATH_TABLE_SIZE, sizeof(AdvertPath));
|
||||
BaseChatMesh::begin();
|
||||
|
||||
if (!_store->loadMainIdentity(self_id)) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v1.6.1"
|
||||
#define FIRMWARE_VERSION "Meck v1.7"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -82,7 +82,7 @@
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
|
||||
struct AdvertPath {
|
||||
uint8_t pubkey_prefix[7];
|
||||
uint8_t pubkey_prefix[8];
|
||||
uint8_t path_len;
|
||||
uint8_t type; // ADV_TYPE_* (Chat/Repeater/Room/Sensor)
|
||||
char name[32];
|
||||
@@ -305,8 +305,8 @@ private:
|
||||
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
|
||||
int next_ack_idx;
|
||||
|
||||
#define ADVERT_PATH_TABLE_SIZE 40
|
||||
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
|
||||
#define ADVERT_PATH_TABLE_SIZE 1000
|
||||
AdvertPath* advert_paths; // PSRAM-allocated in begin(), size = ADVERT_PATH_TABLE_SIZE
|
||||
|
||||
// Sent message repeat tracking
|
||||
#define SENT_TRACK_SIZE 4
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
static uint8_t composeChannelIdx = 0;
|
||||
static unsigned long lastComposeRefresh = 0;
|
||||
static bool composeNeedsRefresh = false;
|
||||
#define COMPOSE_REFRESH_INTERVAL 100 // ms before starting e-ink refresh after keypress (refresh itself takes ~644ms)
|
||||
#define COMPOSE_REFRESH_INTERVAL 800 // ms — must exceed e-ink partial refresh time (~644ms) to prevent back-to-back refreshes
|
||||
|
||||
// Phone dialer debounce — independent from compose/smsSuppressLoop to avoid
|
||||
// interfering with call view rendering and alert display
|
||||
@@ -804,6 +804,22 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read up to 2 touch points — returns actual finger count (0, 1 or 2)
|
||||
static int readTouchMulti(int16_t* outX0, int16_t* outY0, int16_t* outX1, int16_t* outY1) {
|
||||
if (!gt911Ready) return 0;
|
||||
int16_t xs[2], ys[2];
|
||||
uint8_t count = gt911Touch.getPoint(xs, ys, 2);
|
||||
if (count == 0) return 0;
|
||||
if (display.isPortraitMode()) {
|
||||
*outX0 = xs[0]; *outY0 = ys[0];
|
||||
if (count >= 2) { *outX1 = xs[1]; *outY1 = ys[1]; }
|
||||
} else {
|
||||
*outX0 = ys[0]; *outY0 = EPD_HEIGHT - 1 - xs[0];
|
||||
if (count >= 2) { *outX1 = ys[1]; *outY1 = EPD_HEIGHT - 1 - xs[1]; }
|
||||
}
|
||||
return (int)count;
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Shared touch state machine variables ---
|
||||
@@ -828,6 +844,13 @@
|
||||
static bool touchCooldown = false;
|
||||
static unsigned long lastTouchEventMs = 0;
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// Two-finger tap detection state
|
||||
static bool twoFingerDown = false;
|
||||
static unsigned long twoFingerDownTime = 0;
|
||||
#define TWO_FINGER_TAP_MS 350 // max ms from first finger down to both up
|
||||
#endif
|
||||
|
||||
// Unified touch reader — returns physical screen coordinates
|
||||
static bool readTouch(int16_t* outX, int16_t* outY) {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
@@ -2882,6 +2905,51 @@ void loop() {
|
||||
lastTouchSeenMs = now;
|
||||
}
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// Two-finger tap detection — must run before single-finger state machine
|
||||
// so it can suppress the single-finger path when two fingers are present.
|
||||
{
|
||||
int16_t x0, y0, x1, y1;
|
||||
int fingers = readTouchMulti(&x0, &y0, &x1, &y1);
|
||||
if (fingers >= 2) {
|
||||
if (!twoFingerDown) {
|
||||
twoFingerDown = true;
|
||||
twoFingerDownTime = now;
|
||||
}
|
||||
// Suppress single-finger tracking while two fingers are down
|
||||
lastTouchSeenMs = now;
|
||||
touchDown = false;
|
||||
longPressHandled = true; // prevent long-press triggering
|
||||
swipeHandled = true; // prevent swipe triggering
|
||||
} else if (twoFingerDown) {
|
||||
// Both fingers lifted — check if it was a quick tap
|
||||
if ((now - twoFingerDownTime) < TWO_FINGER_TAP_MS) {
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
if (cs) {
|
||||
if (cs->isInSelectMode()) {
|
||||
cs->exitSelectMode();
|
||||
Serial.println("[Touch] Two-finger tap: exited contacts select mode");
|
||||
} else {
|
||||
cs->enterSelectMode();
|
||||
Serial.println("[Touch] Two-finger tap: entered contacts select mode");
|
||||
}
|
||||
ui_task.forceRefresh();
|
||||
touchCooldown = true;
|
||||
lastTouchEventMs = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
twoFingerDown = false;
|
||||
longPressHandled = false;
|
||||
swipeHandled = false;
|
||||
touchDown = false;
|
||||
touchCooldown = true;
|
||||
lastTouchEventMs = now;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool fingerPresent = (now - lastTouchSeenMs) < TOUCH_LIFT_DEBOUNCE_MS;
|
||||
|
||||
if (touchCooldown) {
|
||||
|
||||
@@ -39,7 +39,7 @@ private:
|
||||
uint16_t* _filteredIdx; // indices into contact table
|
||||
uint32_t* _filteredTs; // cached lastmod for sorting
|
||||
int _filteredCount; // how many contacts match current filter
|
||||
AdvertPath _hopBuf[12]; // recently heard advert paths for hop-count display
|
||||
AdvertPath _hopBuf[40]; // recently heard advert paths for hop-count display
|
||||
int _hopBufCount;
|
||||
bool _cacheValid;
|
||||
|
||||
@@ -120,7 +120,7 @@ private:
|
||||
}
|
||||
_cacheValid = true;
|
||||
// Refresh hop-count cache from the 12 most recently heard adverts
|
||||
_hopBufCount = the_mesh.getRecentlyHeard(_hopBuf, 12);
|
||||
_hopBufCount = the_mesh.getRecentlyHeard(_hopBuf, 40);
|
||||
// Clamp scroll position
|
||||
if (_scrollPos >= _filteredCount) {
|
||||
_scrollPos = (_filteredCount > 0) ? _filteredCount - 1 : 0;
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
#define EMOJI_SM_W 10
|
||||
#define EMOJI_SM_H 10
|
||||
|
||||
#define EMOJI_COUNT 65
|
||||
#define EMOJI_COUNT 76
|
||||
|
||||
// Escape codes in 0x80+ range - safe from keyboard ASCII (32-126)
|
||||
#define EMOJI_ESCAPE_START 0x80
|
||||
#define EMOJI_ESCAPE_END 0xC0 // 0x80 + 64
|
||||
#define EMOJI_ESCAPE_END 0xCB // 0x80 + 75
|
||||
#define EMOJI_PAD_BYTE 0x7F // DEL, not typeable (key < 127 guard)
|
||||
|
||||
// ======== LARGE 12x12 SPRITES ========
|
||||
@@ -36,6 +36,14 @@ static const uint8_t emoji_lg_thumbsup[] PROGMEM = {
|
||||
static const uint8_t emoji_lg_frown[] PROGMEM = {
|
||||
0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x80,0x10, 0x9F,0x90, 0xA0,0x50, 0x40,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [65] loudly_crying 😭
|
||||
static const uint8_t emoji_lg_loudly_crying[] PROGMEM = {
|
||||
0x1F,0x80, 0x20,0x40, 0x5B,0x40, 0x5B,0x20, 0x80,0x10, 0x9F,0x10, 0xA0,0x90, 0x60,0xC0, 0xA0,0xA0, 0x1F,0x00, 0x40,0x40, 0x00,0x00,
|
||||
};
|
||||
// [66] heart ♥️
|
||||
static const uint8_t emoji_lg_heart[] PROGMEM = {
|
||||
0x00,0x00, 0x73,0x80, 0xFF,0xC0, 0xFF,0xC0, 0xFF,0xC0, 0x7F,0x80, 0x3F,0x00, 0x1E,0x00, 0x0C,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [3] wireless
|
||||
static const uint8_t emoji_lg_wireless[] PROGMEM = {
|
||||
0x00,0x00, 0x3F,0xC0, 0x60,0x60, 0xC0,0x30, 0x0F,0x00, 0x19,0x80, 0x30,0xC0, 0x00,0x00, 0x06,0x00, 0x0F,0x00, 0x06,0x00, 0x00,0x00,
|
||||
@@ -284,6 +292,43 @@ static const uint8_t emoji_lg_tipping[] PROGMEM = {
|
||||
static const uint8_t emoji_lg_hedgehog[] PROGMEM = {
|
||||
0x00,0x00, 0x0A,0x80, 0x15,0x40, 0x2A,0xA0, 0x55,0x60, 0x7E,0xF0, 0xDB,0x90, 0xFF,0xD0, 0x7F,0xE0, 0x3F,0xC0, 0x24,0x80, 0x00,0x00,
|
||||
};
|
||||
// [67] diamond_suit ♦️
|
||||
static const uint8_t emoji_lg_diamond_suit[] PROGMEM = {
|
||||
0x00,0x00, 0x04,0x00, 0x0E,0x00, 0x1F,0x00, 0x3F,0x80, 0x7F,0xC0, 0x3F,0x80, 0x1F,0x00, 0x0E,0x00, 0x04,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [68] spade_suit ♠️
|
||||
static const uint8_t emoji_lg_spade_suit[] PROGMEM = {
|
||||
0x04,0x00, 0x0E,0x00, 0x1F,0x00, 0x3F,0x80, 0x7F,0xC0, 0xFF,0xE0, 0xFF,0xE0, 0x7F,0xC0, 0x15,0x00, 0x04,0x00, 0x0E,0x00, 0x00,0x00,
|
||||
};
|
||||
// [69] pizza 🍕
|
||||
static const uint8_t emoji_lg_pizza[] PROGMEM = {
|
||||
0x02,0x00, 0x06,0x00, 0x0F,0x00, 0x0B,0x00, 0x1F,0x80, 0x1D,0x80, 0x3F,0xC0, 0x2F,0x40, 0x7F,0xE0, 0x7F,0xE0, 0xFF,0xF0, 0x00,0x00,
|
||||
};
|
||||
// [70] four_leaf_clover 🍀
|
||||
static const uint8_t emoji_lg_four_leaf_clover[] PROGMEM = {
|
||||
0x0C,0x00, 0x1E,0x00, 0x1E,0x00, 0x6D,0x80, 0xF3,0xC0, 0xF3,0xC0, 0x6D,0x80, 0x1E,0x00, 0x1E,0x00, 0x0C,0x00, 0x06,0x00, 0x00,0x00,
|
||||
};
|
||||
// [71] cloud ☁️
|
||||
static const uint8_t emoji_lg_cloud[] PROGMEM = {
|
||||
0x00,0x00, 0x1C,0x00, 0x3E,0x00, 0x7F,0x80, 0xFF,0xC0, 0xFF,0xE0, 0xFF,0xE0, 0x7F,0xC0, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [72] rocket 🚀
|
||||
static const uint8_t emoji_lg_rocket[] PROGMEM = {
|
||||
0x04,0x00, 0x0E,0x00, 0x1F,0x00, 0x1B,0x00, 0x1B,0x00, 0x3F,0x80, 0x7F,0xC0, 0x5F,0x40, 0x9F,0x20, 0x0E,0x00, 0x15,0x00, 0x00,0x00,
|
||||
};
|
||||
// [73] passport_control 🛂
|
||||
static const uint8_t emoji_lg_passport_control[] PROGMEM = {
|
||||
0x3F,0xC0, 0x40,0x20, 0x46,0x20, 0x4F,0x20, 0x46,0x20, 0x40,0x20, 0x44,0x20, 0x4E,0x20, 0x5F,0x20, 0x40,0x20, 0x3F,0xC0, 0x00,0x00,
|
||||
};
|
||||
// [74] eight_spoked_asterisk ✳️
|
||||
static const uint8_t emoji_lg_eight_spoked_asterisk[] PROGMEM = {
|
||||
0x7F,0xE0, 0x84,0x10, 0xA4,0x90, 0x95,0x10, 0x8E,0x10, 0xFF,0xD0, 0x8E,0x10, 0x95,0x10, 0xA4,0x90, 0x84,0x10, 0x7F,0xE0, 0x00,0x00,
|
||||
};
|
||||
// [75] signal_strength 📶
|
||||
static const uint8_t emoji_lg_signal_strength[] PROGMEM = {
|
||||
0x00,0x20, 0x00,0x20, 0x00,0xA0, 0x00,0xA0, 0x02,0xA0, 0x02,0xA0, 0x0A,0xA0, 0x0A,0xA0, 0x2A,0xA0, 0x2A,0xA0, 0xAA,0xA0, 0xAA,0xA0,
|
||||
};
|
||||
|
||||
|
||||
static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
|
||||
emoji_lg_joy, emoji_lg_thumbsup, emoji_lg_frown,
|
||||
@@ -301,6 +346,10 @@ static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
|
||||
emoji_lg_bulb, emoji_lg_cat, emoji_lg_fleur, emoji_lg_moon,
|
||||
emoji_lg_coffee, emoji_lg_tooth, emoji_lg_pretzel, emoji_lg_abacus,
|
||||
emoji_lg_moai, emoji_lg_tipping, emoji_lg_hedgehog,
|
||||
emoji_lg_loudly_crying, emoji_lg_heart, emoji_lg_diamond_suit,
|
||||
emoji_lg_spade_suit, emoji_lg_pizza, emoji_lg_four_leaf_clover,
|
||||
emoji_lg_cloud, emoji_lg_rocket, emoji_lg_passport_control,
|
||||
emoji_lg_eight_spoked_asterisk, emoji_lg_signal_strength,
|
||||
};
|
||||
|
||||
// ======== SMALL 10x10 SPRITES ========
|
||||
@@ -519,6 +568,50 @@ static const uint8_t emoji_sm_tipping[] PROGMEM = {
|
||||
static const uint8_t emoji_sm_hedgehog[] PROGMEM = {
|
||||
0x15,0x00, 0x2A,0x80, 0x55,0x40, 0xFF,0xC0, 0xDB,0x40, 0xFF,0x80, 0x7F,0x80, 0x3F,0x00, 0x24,0x00, 0x00,0x00,
|
||||
};
|
||||
// [65] loudly_crying 😭
|
||||
static const uint8_t emoji_sm_loudly_crying[] PROGMEM = {
|
||||
0x3E,0x00, 0x41,0x00, 0xB6,0x80, 0x80,0x40, 0xBE,0x40, 0x81,0x40, 0x63,0x00, 0x9C,0x80, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [66] heart ♥️
|
||||
static const uint8_t emoji_sm_heart[] PROGMEM = {
|
||||
0x00,0x00, 0x6C,0x00, 0xFE,0x00, 0xFE,0x00, 0x7C,0x00, 0x38,0x00, 0x10,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [67] diamond_suit ♦️
|
||||
static const uint8_t emoji_sm_diamond_suit[] PROGMEM = {
|
||||
0x00,0x00, 0x08,0x00, 0x1C,0x00, 0x3E,0x00, 0x7F,0x00, 0x3E,0x00, 0x1C,0x00, 0x08,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [68] spade_suit ♠️
|
||||
static const uint8_t emoji_sm_spade_suit[] PROGMEM = {
|
||||
0x08,0x00, 0x1C,0x00, 0x3E,0x00, 0x7F,0x00, 0xFF,0x80, 0xFF,0x80, 0x2A,0x00, 0x08,0x00, 0x1C,0x00, 0x00,0x00,
|
||||
};
|
||||
// [69] pizza 🍕
|
||||
static const uint8_t emoji_sm_pizza[] PROGMEM = {
|
||||
0x08,0x00, 0x1C,0x00, 0x14,0x00, 0x3E,0x00, 0x36,0x00, 0x6D,0x00, 0x7F,0x00, 0xFF,0x80, 0xFF,0x80, 0x00,0x00,
|
||||
};
|
||||
// [70] four_leaf_clover 🍀
|
||||
static const uint8_t emoji_sm_four_leaf_clover[] PROGMEM = {
|
||||
0x18,0x00, 0x3C,0x00, 0xDB,0x00, 0xE7,0x00, 0xE7,0x00, 0xDB,0x00, 0x3C,0x00, 0x18,0x00, 0x0C,0x00, 0x00,0x00,
|
||||
};
|
||||
// [71] cloud ☁️
|
||||
static const uint8_t emoji_sm_cloud[] PROGMEM = {
|
||||
0x00,0x00, 0x38,0x00, 0x7E,0x00, 0xFF,0x00, 0xFF,0x80, 0xFF,0x80, 0x7F,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
// [72] rocket 🚀
|
||||
static const uint8_t emoji_sm_rocket[] PROGMEM = {
|
||||
0x08,0x00, 0x1C,0x00, 0x36,0x00, 0x36,0x00, 0x7F,0x00, 0xBE,0x80, 0x3E,0x00, 0x1C,0x00, 0x2A,0x00, 0x00,0x00,
|
||||
};
|
||||
// [73] passport_control 🛂
|
||||
static const uint8_t emoji_sm_passport_control[] PROGMEM = {
|
||||
0x7F,0x80, 0x80,0x40, 0x8C,0x40, 0x9E,0x40, 0x8C,0x40, 0x80,0x40, 0x9C,0x40, 0xBE,0x40, 0x80,0x40, 0x7F,0x80,
|
||||
};
|
||||
// [74] eight_spoked_asterisk ✳️
|
||||
static const uint8_t emoji_sm_eight_spoked_asterisk[] PROGMEM = {
|
||||
0x7F,0x80, 0x84,0x40, 0xA5,0x40, 0x9E,0x40, 0xFF,0xC0, 0x9E,0x40, 0xA5,0x40, 0x84,0x40, 0x7F,0x80, 0x00,0x00,
|
||||
};
|
||||
// [75] signal_strength 📶
|
||||
static const uint8_t emoji_sm_signal_strength[] PROGMEM = {
|
||||
0x00,0x80, 0x00,0x80, 0x02,0x80, 0x02,0x80, 0x0A,0x80, 0x0A,0x80, 0x2A,0x80, 0x2A,0x80, 0xAA,0x80, 0xAA,0x80,
|
||||
};
|
||||
|
||||
static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
|
||||
emoji_sm_joy, emoji_sm_thumbsup, emoji_sm_frown,
|
||||
@@ -536,6 +629,17 @@ static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
|
||||
emoji_sm_bulb, emoji_sm_cat, emoji_sm_fleur, emoji_sm_moon,
|
||||
emoji_sm_coffee, emoji_sm_tooth, emoji_sm_pretzel, emoji_sm_abacus,
|
||||
emoji_sm_moai, emoji_sm_tipping, emoji_sm_hedgehog,
|
||||
emoji_sm_loudly_crying,
|
||||
emoji_sm_heart,
|
||||
emoji_sm_diamond_suit,
|
||||
emoji_sm_spade_suit,
|
||||
emoji_sm_pizza,
|
||||
emoji_sm_four_leaf_clover,
|
||||
emoji_sm_cloud,
|
||||
emoji_sm_rocket,
|
||||
emoji_sm_passport_control,
|
||||
emoji_sm_eight_spoked_asterisk,
|
||||
emoji_sm_signal_strength,
|
||||
};
|
||||
|
||||
// ---- Codepoint lookup for UTF-8 conversion ----
|
||||
@@ -607,6 +711,17 @@ static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = {
|
||||
{ 0x1F5FF, 0x0000, 0xBE }, // moai
|
||||
{ 0x1F481, 0x0000, 0xBF }, // tipping
|
||||
{ 0x1F994, 0x0000, 0xC0 }, // hedgehog
|
||||
{ 0x1F62D , 0x0000 , 0xC1 }, // loudly_crying
|
||||
{ 0x2665 , 0x0000 , 0xC2 }, // heart
|
||||
{ 0x2666 , 0x0000 , 0xC3 }, // diamond_suit
|
||||
{ 0x2660 , 0x0000 , 0xC4 }, // spade_suit
|
||||
{ 0x1F355 , 0x0000 , 0xC5 }, // pizza
|
||||
{ 0x1F340 , 0x0000 , 0xC6 }, // four_leaf_clover
|
||||
{ 0x2601 , 0x0000 , 0xC7 }, // cloud
|
||||
{ 0x1F680 , 0x0000 , 0xC8 }, // rocket
|
||||
{ 0x1F6C2 , 0x0000 , 0xC9 }, // passport_control
|
||||
{ 0x2733 , 0x0000 , 0xCA }, // eight_spoked_asterisk
|
||||
{ 0x1F4F6 , 0x0000 , 0xCB }, // signal_strength
|
||||
};
|
||||
|
||||
// ---- Helper functions ----
|
||||
|
||||
@@ -13,12 +13,15 @@ extern MyMesh the_mesh;
|
||||
// recency. Unlike Discovery (active zero-hop scan), this is purely passive
|
||||
// — it shows nodes whose adverts have been received over time.
|
||||
// ==========================================================================
|
||||
// Display cap — we never need to show all 200 storage entries at once
|
||||
#define LAST_HEARD_DISPLAY_SIZE 100
|
||||
|
||||
class LastHeardScreen : public UIScreen {
|
||||
mesh::RTCClock* _rtc;
|
||||
int _scrollPos;
|
||||
|
||||
// Local sorted copy of advert paths (refreshed each render)
|
||||
AdvertPath _entries[ADVERT_PATH_TABLE_SIZE];
|
||||
// Local sorted copy of advert paths (PSRAM-allocated, refreshed each render)
|
||||
AdvertPath* _entries;
|
||||
int _count;
|
||||
|
||||
static char typeChar(uint8_t adv_type) {
|
||||
@@ -46,7 +49,9 @@ class LastHeardScreen : public UIScreen {
|
||||
|
||||
public:
|
||||
LastHeardScreen(mesh::RTCClock* rtc)
|
||||
: _rtc(rtc), _scrollPos(0), _count(0) {}
|
||||
: _rtc(rtc), _scrollPos(0), _count(0) {
|
||||
_entries = (AdvertPath*)ps_calloc(LAST_HEARD_DISPLAY_SIZE, sizeof(AdvertPath));
|
||||
}
|
||||
|
||||
void resetScroll() { _scrollPos = 0; }
|
||||
|
||||
@@ -91,7 +96,7 @@ public:
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
// Refresh sorted list from mesh
|
||||
_count = the_mesh.getRecentlyHeard(_entries, ADVERT_PATH_TABLE_SIZE);
|
||||
_count = the_mesh.getRecentlyHeard(_entries, LAST_HEARD_DISPLAY_SIZE);
|
||||
|
||||
// Filter out empty entries (recv_timestamp == 0)
|
||||
int validCount = 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
// Emoji Picker with scrolling grid and scroll bar
|
||||
// 5 columns, 4 visible rows, scrollable through all 65 emoji
|
||||
// 5 columns, 4 visible rows, scrollable through all 76 emoji
|
||||
// WASD navigation, Enter to select, $/Q/Backspace to cancel
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
@@ -77,6 +77,17 @@ static const char* EMOJI_LABELS[EMOJI_COUNT] = {
|
||||
"Moai", // 62 moai
|
||||
"Hiii", // 63 tipping
|
||||
"Hedg", // 64 hedgehog
|
||||
"Cry", // 65 loudly_crying
|
||||
"Love", // 66 heart
|
||||
"Diam", // 67 diamond_suit
|
||||
"Spde", // 68 spade_suit
|
||||
"Piza", // 69 pizza
|
||||
"Luck", // 70 four_leaf_clover
|
||||
"Cld", // 71 cloud
|
||||
"Rckt", // 72 rocket
|
||||
"Visa", // 73 passport_control
|
||||
"Star", // 74 eight_spoked_asterisk
|
||||
"Sig", // 75 signal_strength
|
||||
};
|
||||
|
||||
struct EmojiPicker {
|
||||
|
||||
@@ -199,11 +199,16 @@ bool TDeckBoard::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
// Design Capacity correct, but check if Full Charge Capacity is sane.
|
||||
uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP);
|
||||
Serial.printf("BQ27220: Design Capacity already correct, FCC=%d mAh\n", fcc);
|
||||
if (fcc > designCapacity_mAh) {
|
||||
// Check if FCC is outside an acceptable band around design capacity.
|
||||
// Catches both: FCC too high (stale factory 3000mAh) and FCC too low
|
||||
// (gauge learned on a smaller battery, e.g. 1400mAh on a 2500mAh pack).
|
||||
uint16_t fccLo = (designCapacity_mAh > 100) ? designCapacity_mAh - 100 : 0;
|
||||
uint16_t fccHi = designCapacity_mAh + 100;
|
||||
if (fcc < fccLo || fcc > fccHi) {
|
||||
// FCC is >=150% of design — stale from factory defaults (typically 3000 mAh).
|
||||
uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10);
|
||||
Serial.printf("BQ27220: FCC %d >> DC %d, checking Design Energy (target %d mWh)\n",
|
||||
fcc, designCapacity_mAh, designEnergy);
|
||||
Serial.printf("BQ27220: FCC %d outside target band [%d..%d], checking Design Energy (target %d mWh)\n",
|
||||
fcc, fccLo, fccHi, designEnergy);
|
||||
|
||||
// Unseal to read data memory and issue RESET
|
||||
bq27220_writeControl(0x0414); delay(2);
|
||||
|
||||
@@ -157,7 +157,7 @@ build_flags =
|
||||
-D MECK_AUDIO_VARIANT
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.1.WiFi"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.7.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -222,7 +222,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.1.4G"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.7.4G"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -258,7 +258,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.1.4G.WiFi"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.7.4G.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -291,7 +291,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.1.4G.SA"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.7.4G.SA"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
-<helpers/esp32/SerialBLEInterface.cpp>
|
||||
|
||||
Reference in New Issue
Block a user