8 Commits

17 changed files with 846 additions and 464 deletions

View File

@@ -19,6 +19,34 @@ The T-Deck Pro BLE companion firmware includes full keyboard support for standal
| R | Open e-book reader |
| Q | Back to home screen |
### Bluetooth (BLE)
BLE is **disabled by default** at boot to support standalone-first operation. The device is fully functional without a phone — you can send and receive messages, browse contacts, read e-books, and set your timezone directly from the keyboard.
To connect to the MeshCore companion app, navigate to the **Bluetooth** home page (use D to page through) and press **Enter** to toggle BLE on. The BLE PIN will be displayed on screen. Toggle it off again the same way when you're done.
### Clock & Timezone
The T-Deck Pro does not include a dedicated RTC chip, so after each reboot the device clock starts unset. The clock will appear in the nav bar (between node name and battery) once the time has been synced by one of two methods:
1. **GPS fix** (standalone) — Once the GPS acquires a satellite fix, the time is automatically synced from the NMEA data. No phone or BLE connection required. Typical time to first fix is 3090 seconds outdoors with clear sky.
2. **BLE companion app** — If BLE is enabled and connected to the MeshCore companion app, the app will push the current time to the device.
**Setting your timezone:**
Navigate to the **GPS** home page and press **U** to open the UTC offset editor.
| Key | Action |
|-----|--------|
| W | Increase offset (+1 hour) |
| S | Decrease offset (-1 hour) |
| Enter | Save and exit |
| Q | Cancel and exit |
The UTC offset is persisted to flash and survives reboots — you only need to set it once. The valid range is UTC-12 to UTC+14. For example, AEST is UTC+10 and AEDT is UTC+11.
The GPS page also shows the current time, satellite count, position, altitude, and your configured UTC offset for reference.
### Channel Message Screen
| Key | Action |
@@ -36,13 +64,38 @@ Press **N** from the home screen to open the contacts list. All known mesh conta
|-----|--------|
| W / S | Scroll up / down through contacts |
| A / D | Cycle filter: All → Chat → Repeater → Room → Sensor |
| Enter / C | Open DM compose to selected chat contact |
| Enter | Open DM compose (Chat contact) or repeater admin (Repeater contact) |
| C | Open DM compose to selected chat contact |
| Q | Back to home screen |
### Sending a Direct Message
Select a **Chat** contact in the contacts list and press **Enter** or **C** to start composing a direct message. The compose screen will show `DM: ContactName` in the header. Type your message and press **Enter** to send. The DM is sent encrypted directly to that contact (or flooded if no direct path is known). After sending or cancelling, you're returned to the contacts list.
### Repeater Admin Screen
Select a **Repeater** contact in the contacts list and press **Enter** to open the repeater admin screen. You'll be prompted for the repeater's admin password. Characters briefly appear as you type them before being masked, making it easier to enter symbols and numbers on the T-Deck Pro keyboard.
After a successful login, you'll see a menu with the following remote administration commands:
| Menu Item | Description |
|-----------|-------------|
| Clock Sync | Push your device's clock time to the repeater |
| Send Advert | Trigger the repeater to broadcast an advertisement |
| Neighbors | View other repeaters heard via zero-hop adverts |
| Get Clock | Read the repeater's current clock value |
| Version | Query the repeater's firmware version |
| Get Status | Retrieve repeater status information |
| Key | Action |
|-----|--------|
| W / S | Navigate menu items |
| Enter | Execute selected command |
| C | Enter compose mode (send raw CLI command) |
| Q | Back to contacts (from menu) or cancel login |
Command responses are displayed in a scrollable view. Use **W / S** to scroll long responses and **Q** to return to the menu.
### Compose Mode
| Key | Action |
@@ -146,6 +199,8 @@ Download a copy of the Meck firmware bin from https://github.com/pelgraine/Meck/
The companion firmware can be connected to via BLE. USB is planned for a future update.
> **Note:** On the T-Deck Pro, BLE is disabled by default at boot. Navigate to the Bluetooth home page and press Enter to enable BLE before connecting with a companion app.
- Web: https://app.meshcore.nz
- Android: https://play.google.com/store/apps/details?id=com.liamcottle.meshcore.android
- iOS: https://apps.apple.com/us/app/meshcore/id6742354151?platform=iphone
@@ -178,7 +233,8 @@ There are a number of fairly major features in the pipeline, with no particular
- [X] View and compose all channel messages Companion BLE firmware
- [X] Standalone DM functionality for Companion BLE firmware
- [X] Contacts list with filtering for Companion BLE firmware
- [ ] Standalone repeater admin access for Companion BLE firmware
- [X] Standalone repeater admin access for Companion BLE firmware
- [X] GPS time sync with on-device timezone setting
- [ ] Companion radio: USB
- [ ] Simple Repeater firmware for the T-Deck Pro
- [ ] Get pin 45 with the screen backlight functioning for the T-Deck Pro v1.1

View File

@@ -46,4 +46,8 @@ public:
virtual void showAlert(const char* text, int duration_millis) {}
virtual void forceRefresh() {}
virtual void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) {}
// Repeater admin callbacks (from MyMesh)
virtual void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {}
virtual void onAdminCliResponse(const char* from_name, const char* text) {}
};

View File

@@ -228,6 +228,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.read((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88
file.close();
}
@@ -263,6 +264,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.write((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88
file.close();
}
@@ -598,4 +600,4 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
}
return false; // error
}
#endif
#endif

View File

@@ -476,7 +476,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
}
}
return false; // never filter †let normal processing continue
return false; // never filter — let normal processing continue
}
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
@@ -950,6 +950,7 @@ void MyMesh::begin(bool has_display) {
_prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
_prefs.utc_offset_hours = constrain(_prefs.utc_offset_hours, -12, 14); // Valid timezone range
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
@@ -1733,6 +1734,12 @@ void MyMesh::handleCmdFrame(size_t len) {
savePrefs();
}
#endif
// UTC offset for local clock display (works regardless of GPS)
if (strcmp(sp, "utc_offset") == 0) {
int offset = atoi(np);
_prefs.utc_offset_hours = constrain(offset, -12, 14);
savePrefs();
}
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);

View File

@@ -12,7 +12,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.8"
#define FIRMWARE_VERSION "Meck v0.8.2"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -108,6 +108,12 @@ public:
// Send a direct message from the UI (no BLE dependency)
bool uiSendDirectMessage(uint32_t contact_idx, const char* text);
// Repeater admin - UI-initiated operations
bool uiLoginToRepeater(uint32_t contact_idx, const char* password);
bool uiSendCliCommand(uint32_t contact_idx, const char* command);
int getAdminContactIdx() const { return _admin_contact_idx; }
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
@@ -247,6 +253,7 @@ private:
};
SentMsgTrack _sent_track[SENT_TRACK_SIZE];
int _sent_track_idx; // next slot in circular buffer
int _admin_contact_idx; // contact index for active admin session (-1 if none)
};
extern MyMesh the_mesh;

View File

@@ -28,4 +28,5 @@ struct NodePrefs { // persisted to file
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
int8_t utc_offset_hours; // UTC offset in hours (-12 to +14), default 0
};

View File

@@ -379,6 +379,13 @@ void setup() {
MESH_DEBUG_PRINTLN("setup() - GPS enabled by default");
#endif
// T-Deck Pro: BLE starts disabled for standalone-first operation
// User can toggle it on from the Bluetooth home page (Enter or long-press)
#if defined(LilyGo_TDeck_Pro) && defined(BLE_PIN_CODE)
serial_interface.disable();
MESH_DEBUG_PRINTLN("setup() - BLE disabled at boot (standalone mode)");
#endif
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
@@ -760,9 +767,21 @@ void handleKeyboardInput() {
case 'q':
case 'Q':
case '\b':
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
// If editing UTC offset on GPS page, pass through to cancel
if (ui_task.isEditingHomeScreen()) {
ui_task.injectKey('q');
} else {
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
}
break;
case 'u':
case 'U':
// UTC offset editing (on GPS home page)
Serial.println("Nav: UTC offset");
ui_task.injectKey('u');
break;
case ' ':

View File

@@ -0,0 +1,615 @@
#pragma once
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <MeshCore.h>
// Forward declarations
class UITask;
class MyMesh;
extern MyMesh the_mesh;
#define ADMIN_PASSWORD_MAX 32
#define ADMIN_RESPONSE_MAX 512 // CLI responses can be multi-line
#define ADMIN_TIMEOUT_MS 15000 // 15s timeout for login/commands
class RepeaterAdminScreen : public UIScreen {
public:
enum AdminState {
STATE_PASSWORD_ENTRY, // Typing admin password
STATE_LOGGING_IN, // Waiting for login response
STATE_MENU, // Main admin menu
STATE_COMMAND_PENDING, // Waiting for CLI response
STATE_RESPONSE_VIEW, // Displaying CLI response
STATE_ERROR // Error state (timeout, send fail)
};
// Menu items
enum MenuItem {
MENU_CLOCK_SYNC = 0,
MENU_ADVERT,
MENU_NEIGHBORS,
MENU_GET_CLOCK,
MENU_GET_VER,
MENU_GET_STATUS,
MENU_COUNT
};
private:
UITask* _task;
mesh::RTCClock* _rtc;
AdminState _state;
int _contactIdx; // Contact table index of the repeater
char _repeaterName[32]; // Cached repeater name
uint8_t _permissions; // Login permissions (0=guest, 3=admin)
uint32_t _serverTime; // Server timestamp from login response
// Password entry
char _password[ADMIN_PASSWORD_MAX];
int _pwdLen;
unsigned long _lastCharAt; // millis() when last char typed (for brief reveal)
// Menu
int _menuSel; // Currently selected menu item
// Response buffer
char _response[ADMIN_RESPONSE_MAX];
int _responseLen;
int _responseScroll; // Scroll offset for long responses
// Timing
unsigned long _cmdSentAt; // millis() when command was sent
bool _waitingForLogin;
// Password cache - remembers passwords per repeater within session
static const int PWD_CACHE_SIZE = 8;
struct PwdCacheEntry {
int contactIdx;
char password[ADMIN_PASSWORD_MAX];
};
PwdCacheEntry _pwdCache[PWD_CACHE_SIZE];
int _pwdCacheCount;
// Look up cached password for a contact, returns nullptr if not found
const char* getCachedPassword(int contactIdx) {
for (int i = 0; i < _pwdCacheCount; i++) {
if (_pwdCache[i].contactIdx == contactIdx) return _pwdCache[i].password;
}
return nullptr;
}
// Save password to cache (update existing or add new, evict oldest if full)
void cachePassword(int contactIdx, const char* pwd) {
// Update existing entry
for (int i = 0; i < _pwdCacheCount; i++) {
if (_pwdCache[i].contactIdx == contactIdx) {
strncpy(_pwdCache[i].password, pwd, ADMIN_PASSWORD_MAX - 1);
_pwdCache[i].password[ADMIN_PASSWORD_MAX - 1] = '\0';
return;
}
}
// Add new entry, evict oldest if full
if (_pwdCacheCount < PWD_CACHE_SIZE) {
int slot = _pwdCacheCount++;
_pwdCache[slot].contactIdx = contactIdx;
strncpy(_pwdCache[slot].password, pwd, ADMIN_PASSWORD_MAX - 1);
_pwdCache[slot].password[ADMIN_PASSWORD_MAX - 1] = '\0';
} else {
// Shift entries down to evict oldest
for (int i = 0; i < PWD_CACHE_SIZE - 1; i++) {
_pwdCache[i] = _pwdCache[i + 1];
}
_pwdCache[PWD_CACHE_SIZE - 1].contactIdx = contactIdx;
strncpy(_pwdCache[PWD_CACHE_SIZE - 1].password, pwd, ADMIN_PASSWORD_MAX - 1);
_pwdCache[PWD_CACHE_SIZE - 1].password[ADMIN_PASSWORD_MAX - 1] = '\0';
}
}
static const char* menuLabel(MenuItem m) {
switch (m) {
case MENU_CLOCK_SYNC: return "Clock Sync";
case MENU_ADVERT: return "Send Advert";
case MENU_NEIGHBORS: return "Neighbors";
case MENU_GET_CLOCK: return "Get Clock";
case MENU_GET_VER: return "Version";
case MENU_GET_STATUS: return "Get Status";
default: return "?";
}
}
static const char* menuCommand(MenuItem m) {
switch (m) {
case MENU_CLOCK_SYNC: return "clock sync";
case MENU_ADVERT: return "advert";
case MENU_NEIGHBORS: return "neighbors";
case MENU_GET_CLOCK: return "clock";
case MENU_GET_VER: return "ver";
case MENU_GET_STATUS: return "get status";
default: return "";
}
}
// Format epoch as HH:MM:SS
static void formatTime(char* buf, size_t bufLen, uint32_t epoch) {
if (epoch == 0) {
strncpy(buf, "--:--:--", bufLen);
return;
}
uint32_t secs = epoch % 60;
uint32_t mins = (epoch / 60) % 60;
uint32_t hrs = (epoch / 3600) % 24;
snprintf(buf, bufLen, "%02d:%02d:%02d", (int)hrs, (int)mins, (int)secs);
}
public:
RepeaterAdminScreen(UITask* task, mesh::RTCClock* rtc)
: _task(task), _rtc(rtc), _state(STATE_PASSWORD_ENTRY),
_contactIdx(-1), _permissions(0), _serverTime(0),
_pwdLen(0), _lastCharAt(0), _menuSel(0),
_responseLen(0), _responseScroll(0),
_cmdSentAt(0), _waitingForLogin(false), _pwdCacheCount(0) {
_password[0] = '\0';
_repeaterName[0] = '\0';
_response[0] = '\0';
}
// Called when entering the screen for a specific repeater contact
void openForContact(int contactIdx, const char* name) {
_contactIdx = contactIdx;
strncpy(_repeaterName, name, sizeof(_repeaterName) - 1);
_repeaterName[sizeof(_repeaterName) - 1] = '\0';
// Reset state
_state = STATE_PASSWORD_ENTRY;
_lastCharAt = 0;
_menuSel = 0;
_permissions = 0;
_serverTime = 0;
_responseLen = 0;
_responseScroll = 0;
_response[0] = '\0';
_waitingForLogin = false;
// Pre-fill from password cache if available
const char* cached = getCachedPassword(contactIdx);
if (cached) {
strncpy(_password, cached, ADMIN_PASSWORD_MAX - 1);
_password[ADMIN_PASSWORD_MAX - 1] = '\0';
_pwdLen = strlen(_password);
} else {
_pwdLen = 0;
_password[0] = '\0';
}
}
int getContactIdx() const { return _contactIdx; }
AdminState getState() const { return _state; }
// Called by UITask when a login response is received
void onLoginResult(bool success, uint8_t permissions, uint32_t server_time) {
_waitingForLogin = false;
if (success) {
_permissions = permissions;
_serverTime = server_time;
_state = STATE_MENU;
cachePassword(_contactIdx, _password); // remember for next time
} else {
snprintf(_response, sizeof(_response), "Login failed.\nCheck password.");
_responseLen = strlen(_response);
_state = STATE_ERROR;
}
}
// Called by UITask when a CLI response is received
void onCliResponse(const char* text) {
if (_state != STATE_COMMAND_PENDING) return;
int tlen = strlen(text);
if (tlen >= ADMIN_RESPONSE_MAX) tlen = ADMIN_RESPONSE_MAX - 1;
memcpy(_response, text, tlen);
_response[tlen] = '\0';
_responseLen = tlen;
_responseScroll = 0;
_state = STATE_RESPONSE_VIEW;
}
// Poll for timeouts
void poll() override {
if ((_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) &&
_cmdSentAt > 0 && (millis() - _cmdSentAt) > ADMIN_TIMEOUT_MS) {
snprintf(_response, sizeof(_response), "Timeout - no response.");
_responseLen = strlen(_response);
_state = STATE_ERROR;
}
}
int render(DisplayDriver& display) override {
char tmp[64];
// === Header ===
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, 0);
// Truncate name if needed to fit header
snprintf(tmp, sizeof(tmp), "Admin: %.16s", _repeaterName);
display.print(tmp);
// Show permissions if logged in
if (_state >= STATE_MENU && _state <= STATE_RESPONSE_VIEW) {
const char* perm = (_permissions & 0x03) >= 3 ? "ADM" :
(_permissions & 0x03) >= 2 ? "R/W" : "R/O";
display.setCursor(display.width() - display.getTextWidth(perm) - 2, 0);
display.print(perm);
}
display.drawRect(0, 11, display.width(), 1); // divider
// === Body - depends on state ===
int bodyY = 14;
int footerY = display.height() - 12;
int bodyHeight = footerY - bodyY - 4;
switch (_state) {
case STATE_PASSWORD_ENTRY:
renderPasswordEntry(display, bodyY);
break;
case STATE_LOGGING_IN:
renderWaiting(display, bodyY, "Logging in...");
break;
case STATE_MENU:
renderMenu(display, bodyY, bodyHeight);
break;
case STATE_COMMAND_PENDING:
renderWaiting(display, bodyY, "Waiting...");
break;
case STATE_RESPONSE_VIEW:
renderResponse(display, bodyY, bodyHeight);
break;
case STATE_ERROR:
renderResponse(display, bodyY, bodyHeight); // reuse response renderer for errors
break;
}
// === Footer ===
display.drawRect(0, footerY - 2, display.width(), 1);
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(1);
display.setCursor(0, footerY);
switch (_state) {
case STATE_PASSWORD_ENTRY:
display.print("Q:Back");
{
const char* right = "Enter:Login";
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);
}
break;
case STATE_LOGGING_IN:
case STATE_COMMAND_PENDING:
display.print("Q:Cancel");
break;
case STATE_MENU:
display.print("Q:Back");
{
const char* mid = "W/S:Sel";
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
display.print(mid);
const char* right = "Ent:Run";
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);
}
break;
case STATE_RESPONSE_VIEW:
case STATE_ERROR:
display.print("Q:Menu");
if (_responseLen > bodyHeight / 9) { // if scrollable
const char* right = "W/S:Scrll";
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);
}
break;
}
if (_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) return 1000;
// During password reveal, refresh when the reveal expires
if (_state == STATE_PASSWORD_ENTRY && _lastCharAt > 0 && (millis() - _lastCharAt) < 800) {
return _lastCharAt + 800 - millis() + 50; // refresh shortly after reveal ends
}
return 5000;
}
bool handleInput(char c) override {
switch (_state) {
case STATE_PASSWORD_ENTRY:
return handlePasswordInput(c);
case STATE_LOGGING_IN:
case STATE_COMMAND_PENDING:
// Q to cancel and go back
if (c == 'q' || c == 'Q') {
_state = (_state == STATE_LOGGING_IN) ? STATE_PASSWORD_ENTRY : STATE_MENU;
return true;
}
return false;
case STATE_MENU:
return handleMenuInput(c);
case STATE_RESPONSE_VIEW:
case STATE_ERROR:
return handleResponseInput(c);
}
return false;
}
private:
// --- Password Entry ---
void renderPasswordEntry(DisplayDriver& display, int y) {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, y);
display.print("Password:");
y += 14;
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, y);
// Show asterisks for password characters, with brief reveal of last char
char masked[ADMIN_PASSWORD_MAX];
int i;
bool revealing = (_pwdLen > 0 && (millis() - _lastCharAt) < 800);
int revealIdx = revealing ? _pwdLen - 1 : -1;
for (i = 0; i < _pwdLen && i < ADMIN_PASSWORD_MAX - 1; i++) {
masked[i] = (i == revealIdx) ? _password[i] : '*';
}
masked[i] = '\0';
display.print(masked);
// Cursor indicator
display.print("_");
}
bool handlePasswordInput(char c) {
// Q without any password typed = go back (return false to signal "not handled")
if ((c == 'q' || c == 'Q') && _pwdLen == 0) {
return false;
}
// Enter to submit
if (c == '\r' || c == '\n' || c == KEY_ENTER) {
if (_pwdLen > 0) {
return doLogin();
}
return true;
}
// Backspace
if (c == 0x08 || c == 0x7F) {
if (_pwdLen > 0) {
_pwdLen--;
_password[_pwdLen] = '\0';
_lastCharAt = 0; // no reveal after delete
}
return true;
}
// Printable character
if (c >= 32 && c < 127 && _pwdLen < ADMIN_PASSWORD_MAX - 1) {
_password[_pwdLen++] = c;
_password[_pwdLen] = '\0';
_lastCharAt = millis(); // start brief reveal
return true;
}
return false;
}
bool doLogin(); // Defined below, calls into MyMesh
// --- Menu ---
void renderMenu(DisplayDriver& display, int y, int bodyHeight) {
display.setTextSize(0); // tiny font for compact rows
int lineHeight = 9;
// Show server time comparison if available
if (_serverTime > 0) {
char ourTime[12], srvTime[12];
uint32_t now = _rtc->getCurrentTime();
formatTime(ourTime, sizeof(ourTime), now);
formatTime(srvTime, sizeof(srvTime), _serverTime);
int drift = (int)(now - _serverTime);
char driftStr[24];
if (abs(drift) < 2) {
snprintf(driftStr, sizeof(driftStr), "Synced");
} else {
snprintf(driftStr, sizeof(driftStr), "Drift:%+ds", drift);
}
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, y);
char info[48];
snprintf(info, sizeof(info), "Rpt:%s Us:%s %s", srvTime, ourTime, driftStr);
display.print(info);
y += lineHeight + 2;
}
// Menu items
for (int i = 0; i < MENU_COUNT && y + lineHeight <= display.height() - 16; i++) {
bool selected = (i == _menuSel);
if (selected) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y + 5, display.width(), lineHeight);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(2, y);
char label[32];
snprintf(label, sizeof(label), "%s %s", selected ? ">" : " ", menuLabel((MenuItem)i));
display.print(label);
y += lineHeight;
}
display.setTextSize(1);
}
bool handleMenuInput(char c) {
// W/up - scroll up
if (c == 'w' || c == 'W' || c == 0xF2) {
if (_menuSel > 0) _menuSel--;
return true;
}
// S/down - scroll down
if (c == 's' || c == 'S' || c == 0xF1) {
if (_menuSel < MENU_COUNT - 1) _menuSel++;
return true;
}
// Enter - execute selected command
if (c == '\r' || c == '\n' || c == KEY_ENTER) {
return executeMenuCommand((MenuItem)_menuSel);
}
// Q - back to contacts
if (c == 'q' || c == 'Q') {
return false; // let UITask handle back navigation
}
// Number keys for quick selection
if (c >= '1' && c <= '0' + MENU_COUNT) {
_menuSel = c - '1';
return executeMenuCommand((MenuItem)_menuSel);
}
return false;
}
bool executeMenuCommand(MenuItem item); // Defined below, calls into MyMesh
// --- Response View ---
void renderResponse(DisplayDriver& display, int y, int bodyHeight) {
display.setTextSize(0); // tiny font for more content
int lineHeight = 9;
display.setColor((_state == STATE_ERROR) ? DisplayDriver::YELLOW : DisplayDriver::LIGHT);
// Render response text with word wrapping and scroll support
int maxLines = bodyHeight / lineHeight;
int lineCount = 0;
int skipLines = _responseScroll;
const char* p = _response;
char lineBuf[80];
int lineWidth = display.width() - 4;
while (*p && lineCount < maxLines + skipLines) {
// Extract next line (up to newline or screen width)
int i = 0;
while (*p && *p != '\n' && i < 79) {
lineBuf[i++] = *p++;
}
lineBuf[i] = '\0';
if (*p == '\n') p++;
if (lineCount >= skipLines && lineCount < skipLines + maxLines) {
display.setCursor(2, y);
display.print(lineBuf);
y += lineHeight;
}
lineCount++;
}
display.setTextSize(1);
}
bool handleResponseInput(char c) {
// W/up - scroll up
if (c == 'w' || c == 'W' || c == 0xF2) {
if (_responseScroll > 0) {
_responseScroll--;
return true;
}
}
// S/down - scroll down
if (c == 's' || c == 'S' || c == 0xF1) {
_responseScroll++;
return true;
}
// Q - back to menu (or back to password on error)
if (c == 'q' || c == 'Q') {
if (_state == STATE_ERROR && _permissions == 0) {
// Not yet logged in, go back to password
_state = STATE_PASSWORD_ENTRY;
} else {
_state = STATE_MENU;
}
return true;
}
// Enter - also go back to menu
if (c == '\r' || c == '\n' || c == KEY_ENTER) {
_state = STATE_MENU;
return true;
}
return false;
}
// --- Waiting spinner ---
void renderWaiting(DisplayDriver& display, int y, const char* msg) {
display.setTextSize(1);
display.setColor(DisplayDriver::YELLOW);
int cx = (display.width() - display.getTextWidth(msg)) / 2;
int cy = y + 20;
display.setCursor(cx, cy);
display.print(msg);
// Show elapsed time
if (_cmdSentAt > 0) {
char elapsed[16];
unsigned long secs = (millis() - _cmdSentAt) / 1000;
snprintf(elapsed, sizeof(elapsed), "%lus", secs);
display.setColor(DisplayDriver::LIGHT);
display.setCursor((display.width() - display.getTextWidth(elapsed)) / 2, cy + 14);
display.print(elapsed);
}
}
};
// --- Implementations that require MyMesh (the_mesh is declared extern above) ---
inline bool RepeaterAdminScreen::doLogin() {
if (_contactIdx < 0 || _pwdLen == 0) return false;
if (the_mesh.uiLoginToRepeater(_contactIdx, _password)) {
_state = STATE_LOGGING_IN;
_cmdSentAt = millis();
_waitingForLogin = true;
return true;
} else {
snprintf(_response, sizeof(_response), "Send failed.\nCheck contact path.");
_responseLen = strlen(_response);
_state = STATE_ERROR;
return true;
}
}
inline bool RepeaterAdminScreen::executeMenuCommand(MenuItem item) {
if (_contactIdx < 0) return false;
const char* cmd = menuCommand(item);
if (cmd[0] == '\0') return false;
if (the_mesh.uiSendCliCommand(_contactIdx, cmd)) {
_state = STATE_COMMAND_PENDING;
_cmdSentAt = millis();
_response[0] = '\0';
_responseLen = 0;
_responseScroll = 0;
return true;
} else {
snprintf(_response, sizeof(_response), "Send failed.");
_responseLen = strlen(_response);
_state = STATE_ERROR;
return true;
}
}

View File

@@ -102,6 +102,8 @@ class HomeScreen : public UIScreen {
NodePrefs* _node_prefs;
uint8_t _page;
bool _shutdown_init;
bool _editing_utc;
int8_t _saved_utc_offset; // for cancel/undo
AdvertPath recent[UI_RECENT_LIST_SIZE];
@@ -183,7 +185,15 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts)
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
_shutdown_init(false), _editing_utc(false), _saved_utc_offset(0), sensors_lpp(200) { }
bool isEditingUTC() const { return _editing_utc; }
void cancelEditUTC() {
if (_editing_utc) {
_node_prefs->utc_offset_hours = _saved_utc_offset;
_editing_utc = false;
}
}
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
@@ -204,6 +214,29 @@ public:
// battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts());
// centered clock (tinyfont) - only show when time is valid
{
uint32_t now = _rtc->getCurrentTime();
if (now > 1700000000) { // valid timestamp (after ~Nov 2023)
// Apply UTC offset from prefs
int32_t local = (int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600);
int hrs = (local / 3600) % 24;
if (hrs < 0) hrs += 24;
int mins = (local / 60) % 60;
if (mins < 0) mins += 60;
char timeBuf[6];
sprintf(timeBuf, "%02d:%02d", hrs, mins);
display.setTextSize(0); // tinyfont
display.setColor(DisplayDriver::LIGHT);
uint16_t tw = display.getTextWidth(timeBuf);
int clockX = (display.width() - tw) / 2;
display.setCursor(clockX, -3); // align with battery text Y
display.print(timeBuf);
display.setTextSize(1); // restore
}
}
// curr page indicator
int y = 14;
int x = display.width() / 2 - 5 * (HomePage::Count-1);
@@ -331,6 +364,42 @@ public:
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
}
// Show RTC time and UTC offset on GPS page
{
uint32_t now = _rtc->getCurrentTime();
if (now > 1700000000) {
int32_t local = (int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600);
int hrs = (local / 3600) % 24;
if (hrs < 0) hrs += 24;
int mins = (local / 60) % 60;
if (mins < 0) mins += 60;
display.drawTextLeftAlign(0, y, "time(U)");
sprintf(buf, "%02d:%02d UTC%+d", hrs, mins, _node_prefs->utc_offset_hours);
display.drawTextRightAlign(display.width()-1, y, buf);
} else {
display.drawTextLeftAlign(0, y, "time(U)");
display.drawTextRightAlign(display.width()-1, y, "no sync");
}
}
// UTC offset editor overlay
if (_editing_utc) {
// Draw background box
int bx = 4, by = 20, bw = display.width() - 8, bh = 40;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
// Show current offset value
display.setTextSize(2);
sprintf(buf, "UTC%+d", _node_prefs->utc_offset_hours);
display.drawTextCentered(display.width() / 2, by + 4, buf);
// Show controls hint
display.setTextSize(0);
display.drawTextCentered(display.width() / 2, by + bh - 10, "W/S:adj Enter:ok Q:cancel");
display.setTextSize(1);
}
#endif
#if UI_SENSORS_PAGE == 1
} else if (_page == HomePage::SENSORS) {
@@ -414,10 +483,44 @@ public:
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
}
}
return 5000; // next render after 5000 ms
return _editing_utc ? 700 : 5000; // match e-ink refresh cycle while editing UTC
}
bool handleInput(char c) override {
// UTC offset editing mode - intercept all keys
if (_editing_utc) {
if (c == 'w' || c == KEY_PREV) {
// Increment offset
if (_node_prefs->utc_offset_hours < 14) {
_node_prefs->utc_offset_hours++;
}
return true;
}
if (c == 's' || c == KEY_NEXT) {
// Decrement offset
if (_node_prefs->utc_offset_hours > -12) {
_node_prefs->utc_offset_hours--;
}
return true;
}
if (c == KEY_ENTER) {
// Save and exit
Serial.printf("UTC offset saving: %d\n", _node_prefs->utc_offset_hours);
the_mesh.savePrefs();
_editing_utc = false;
_task->showAlert("UTC offset saved", 800);
Serial.println("UTC offset save complete");
return true;
}
if (c == 'q' || c == 'u') {
// Cancel - restore original value
_node_prefs->utc_offset_hours = _saved_utc_offset;
_editing_utc = false;
return true;
}
return true; // Consume all other keys while editing
}
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
@@ -451,6 +554,11 @@ public:
_task->toggleGPS();
return true;
}
if (c == 'u' && _page == HomePage::GPS) {
_editing_utc = true;
_saved_utc_offset = _node_prefs->utc_offset_hours;
return true;
}
#endif
#if UI_SENSORS_PAGE == 1
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
@@ -986,11 +1094,22 @@ void UITask::injectKey(char c) {
}
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 100; // trigger refresh
// Debounce refresh when editing UTC offset - e-ink takes 644ms per refresh
// so don't queue another render until the current one could have finished
if (isEditingHomeScreen()) {
unsigned long earliest = millis() + 700;
if (_next_refresh < earliest) {
_next_refresh = earliest;
}
} else {
_next_refresh = 100; // trigger refresh
}
}
}
void UITask::gotoHomeScreen() {
// Cancel any active editing state when navigating to home
((HomeScreen *) home)->cancelEditUTC();
setCurrScreen(home);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
@@ -999,6 +1118,10 @@ void UITask::gotoHomeScreen() {
_next_refresh = 100;
}
bool UITask::isEditingHomeScreen() const {
return curr == home && ((HomeScreen *) home)->isEditingUTC();
}
void UITask::gotoChannelScreen() {
((ChannelScreen *) channel_screen)->resetScroll();
setCurrScreen(channel_screen);

View File

@@ -87,6 +87,7 @@ public:
bool isOnChannelScreen() const { return curr == channel_screen; }
bool isOnContactsScreen() const { return curr == contacts_screen; }
bool isOnTextReader() const { return curr == text_reader; } // *** NEW ***
bool isEditingHomeScreen() const; // UTC offset editing on GPS page
uint8_t getChannelScreenViewIdx() const;
void toggleBuzzer();

View File

@@ -1,31 +0,0 @@
#include <Arduino.h>
#include <Wire.h>
#include "TechoBoard.h"
#ifdef LILYGO_TECHO
void TechoBoard::begin() {
NRF52Board::begin();
Wire.begin();
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
}
uint16_t TechoBoard::getBattMilliVolts() {
int adcvalue = 0;
analogReference(AR_INTERNAL_3_0);
analogReadResolution(12);
delay(10);
// ADC range is 0..3000mV and resolution is 12-bit (0..4095)
adcvalue = analogRead(PIN_VBAT_READ);
// Convert the raw value to compensated mv, taking the resistor-
// divider into account (providing the actual LIPO voltage)
return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB);
}
#endif

View File

@@ -1,44 +0,0 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/NRF52Board.h>
// built-ins
#define VBAT_MV_PER_LSB (0.73242188F) // 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096
#define VBAT_DIVIDER (0.5F) // 150K + 150K voltage divider on VBAT
#define VBAT_DIVIDER_COMP (2.0F) // Compensation factor for the VBAT divider
#define PIN_VBAT_READ (4)
#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB)
class TechoBoard : public NRF52BoardOTA {
public:
TechoBoard() : NRF52BoardOTA("TECHO_OTA") {}
void begin();
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override {
return "LilyGo T-Echo";
}
void powerOff() override {
#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();
}
};

View File

@@ -1,98 +0,0 @@
[LilyGo_T-Echo-Lite]
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 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
-D P_LORA_TX_LED=LED_GREEN
-D DISABLE_DIAGNOSTIC_OUTPUT
-D ENV_INCLUDE_GPS=1
-D GPS_BAUD_RATE=9600
-D PIN_GPS_EN=GPS_EN
-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=0
-D EINK_Y_OFFSET=10
-D DISPLAY_ROTATION=4
-D AUTO_OFF_MILLIS=0
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<TechoBoard.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/ui/GxEPDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../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
[env:LilyGo_T-Echo-Lite_repeater]
extends = LilyGo_T-Echo-Lite
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${LilyGo_T-Echo-Lite.build_flags}
-D ADVERT_NAME='"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
[env:LilyGo_T-Echo-Lite_room_server]
extends = LilyGo_T-Echo-Lite
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
+<../examples/simple_room_server>
build_flags =
${LilyGo_T-Echo-Lite.build_flags}
-D ADVERT_NAME='"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
[env:LilyGo_T-Echo-Lite_companion_radio_ble]
extends = LilyGo_T-Echo-Lite
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${LilyGo_T-Echo-Lite.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D QSPIFLASH=1
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
-D UI_RECENT_LIST_SIZE=9
-D UI_SENSORS_PAGE=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${LilyGo_T-Echo-Lite.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -1,52 +0,0 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View File

@@ -1,31 +0,0 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <TechoBoard.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/GxEPDDisplay.h>
#include <helpers/ui/MomentaryButton.h>
#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(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -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);
}

View File

@@ -1,158 +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 (4.90F)
#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(0, 4) // (SDA)
#define PIN_WIRE_SCL _PINNUM(0, 2) // (SCL)
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition
#define SPI_INTERFACES_COUNT _PINNUM(0, 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
#define PIN_GPS_RX _PINNUM(1, 13) // RXD
#define PIN_GPS_TX _PINNUM(1, 15) // TXD
#define GPS_EN _PINNUM(1, 11) // POWER_RT9080_EN
#define PIN_GPS_STANDBY _PINNUM(1, 10)
#define PIN_GPS_PPS _PINNUM(0, 29) // 1PPS