mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-17 16:35:03 +02:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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() {}
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// 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;
|
||||
};
|
||||
@@ -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]))
|
||||
@@ -1,339 +0,0 @@
|
||||
#include "TechoCardBoard.h"
|
||||
#include "variant.h"
|
||||
#include <Wire.h>
|
||||
#include <nrf_soc.h>
|
||||
#include <InternalFileSystem.h>
|
||||
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;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
#include "variant.h"
|
||||
|
||||
#if defined(HAS_RGB_LED)
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#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 };
|
||||
};
|
||||
@@ -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 <math.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#include <target.h>
|
||||
#include "MyMesh.h"
|
||||
#include "UITask.h"
|
||||
|
||||
// =============================================================================
|
||||
// Voice recording -- PDM mic -> Codec2 1200bps stream encoding
|
||||
// =============================================================================
|
||||
#if defined(HAS_MICROPHONE)
|
||||
#include <PDM.h>
|
||||
#include <codec2.h>
|
||||
|
||||
// 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
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
+<../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}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../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}
|
||||
+<helpers/nrf52/*.cpp>
|
||||
+<../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>
|
||||
@@ -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 <InternalFileSystem.h>
|
||||
|
||||
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;
|
||||
@@ -1,58 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#include "TechoCardBoard.h"
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include "GPSStreamCounter.h"
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(USE_U8G2_DISPLAY)
|
||||
#include <helpers/ui/U8g2Display.h>
|
||||
#else
|
||||
#include <helpers/ui/SSD1306Display.h>
|
||||
#endif
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#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();
|
||||
@@ -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};
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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 <FS.h> in shared headers.
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <time.h> // 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
|
||||
@@ -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 <Adafruit_LittleFS.h>
|
||||
#include <InternalFileSystem.h>
|
||||
#include <time.h> // 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<typename... Args>
|
||||
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;
|
||||
@@ -1,54 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#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
|
||||
@@ -1,43 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
|
||||
// ============================================================
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
+<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
|
||||
|
||||
; =============================================================================
|
||||
; 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}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../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}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../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}
|
||||
+<helpers/*.cpp>
|
||||
+<TechoBoard.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.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
|
||||
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}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
lib_deps =
|
||||
${lilygo_techo_lite_meck_core.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
@@ -1,55 +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(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);
|
||||
}
|
||||
@@ -1,32 +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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
void radio_reset_agc();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user