mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-09 23:04:53 +02:00
Heltec Meshpocket: initial BLE companion port
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Heltec nrf (Adafruit BSP)",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_ram_size": 235520,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
|
||||
@@ -504,9 +504,16 @@ bool DataStore::beginSaveContacts(DataStoreHost* host) {
|
||||
if (_saveInProgress) return false; // Already saving
|
||||
|
||||
FILESYSTEM* fs = _getContactsChannelsFS();
|
||||
_saveFile = openWrite(fs, "/contacts3.tmp");
|
||||
if (!_saveFile) {
|
||||
// Defensive cleanup in case a previous save didn't reach finishSaveContacts()
|
||||
if (_saveFile) {
|
||||
_saveFile->close();
|
||||
delete _saveFile;
|
||||
_saveFile = nullptr;
|
||||
}
|
||||
_saveFile = new File(openWrite(fs, "/contacts3.tmp"));
|
||||
if (!_saveFile || !*_saveFile) {
|
||||
Serial.println("DataStore: chunked save FAILED — cannot open tmp file");
|
||||
if (_saveFile) { delete _saveFile; _saveFile = nullptr; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -527,18 +534,18 @@ bool DataStore::saveContactsChunk(int batchSize) {
|
||||
int written = 0;
|
||||
|
||||
while (written < batchSize && _saveHost->getContactForSave(_saveIdx, c)) {
|
||||
bool success = (_saveFile.write(c.id.pub_key, 32) == 32);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.name, 32) == 32);
|
||||
success = success && (_saveFile.write(&c.type, 1) == 1);
|
||||
success = success && (_saveFile.write(&c.flags, 1) == 1);
|
||||
success = success && (_saveFile.write(&unused, 1) == 1);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.sync_since, 4) == 4);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
||||
success = success && (_saveFile.write(c.out_path, 64) == 64);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.lastmod, 4) == 4);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.gps_lat, 4) == 4);
|
||||
success = success && (_saveFile.write((uint8_t *)&c.gps_lon, 4) == 4);
|
||||
bool success = (_saveFile->write(c.id.pub_key, 32) == 32);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.name, 32) == 32);
|
||||
success = success && (_saveFile->write(&c.type, 1) == 1);
|
||||
success = success && (_saveFile->write(&c.flags, 1) == 1);
|
||||
success = success && (_saveFile->write(&unused, 1) == 1);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.sync_since, 4) == 4);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
||||
success = success && (_saveFile->write(c.out_path, 64) == 64);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.lastmod, 4) == 4);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.gps_lat, 4) == 4);
|
||||
success = success && (_saveFile->write((uint8_t *)&c.gps_lon, 4) == 4);
|
||||
|
||||
if (!success) {
|
||||
_saveWriteOk = false;
|
||||
@@ -562,7 +569,11 @@ bool DataStore::saveContactsChunk(int batchSize) {
|
||||
void DataStore::finishSaveContacts() {
|
||||
if (!_saveInProgress) return;
|
||||
|
||||
_saveFile.close();
|
||||
if (_saveFile) {
|
||||
_saveFile->close();
|
||||
delete _saveFile;
|
||||
_saveFile = nullptr;
|
||||
}
|
||||
_saveInProgress = false;
|
||||
|
||||
FILESYSTEM* fs = _getContactsChannelsFS();
|
||||
|
||||
@@ -25,7 +25,10 @@ class DataStore {
|
||||
#endif
|
||||
|
||||
// Chunked save state
|
||||
File _saveFile;
|
||||
// Stored as a pointer (allocated in beginSaveContacts, freed in
|
||||
// finishSaveContacts) because Adafruit_LittleFS::File has no default
|
||||
// constructor — we can't keep one as a default-initialized value member.
|
||||
File* _saveFile = nullptr;
|
||||
DataStoreHost* _saveHost = nullptr;
|
||||
uint32_t _saveIdx = 0;
|
||||
uint32_t _saveRecordsWritten = 0;
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
#include "ModemManager.h" // Serial CLI modem commands
|
||||
#endif
|
||||
|
||||
// Fallback for variants that don't define GPS_BAUDRATE (HAS_GPS=0 boards like
|
||||
// Heltec Meshpocket). Used in CLI "get/set gps.baud" handlers as the default
|
||||
// when node prefs haven't been configured. Zero means "not applicable".
|
||||
#ifndef GPS_BAUDRATE
|
||||
#define GPS_BAUDRATE 0
|
||||
#endif
|
||||
|
||||
#define CMD_APP_START 1
|
||||
#define CMD_SEND_TXT_MSG 2
|
||||
#define CMD_SEND_CHANNEL_TXT_MSG 3
|
||||
@@ -1294,7 +1301,15 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
}
|
||||
|
||||
void MyMesh::begin(bool has_display) {
|
||||
#if defined(ESP32)
|
||||
// ESP32 variants have PSRAM — allocate the large advert path table there
|
||||
advert_paths = (AdvertPath*)ps_calloc(ADVERT_PATH_TABLE_SIZE, sizeof(AdvertPath));
|
||||
#else
|
||||
// nRF52 / other non-PSRAM platforms — fall back to regular heap. Table size
|
||||
// is smaller on these platforms (see ADVERT_PATH_TABLE_SIZE in MyMesh.h) to
|
||||
// avoid blowing the limited SRAM budget.
|
||||
advert_paths = (AdvertPath*)calloc(ADVERT_PATH_TABLE_SIZE, sizeof(AdvertPath));
|
||||
#endif
|
||||
BaseChatMesh::begin();
|
||||
|
||||
if (!_store->loadMainIdentity(self_id)) {
|
||||
@@ -3286,4 +3301,20 @@ bool MyMesh::addDiscoveredToContacts(int idx) {
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("Discovery: no cached advert blob for contact '%s'", _discovered[idx].contact.name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#ifdef HELTEC_MESH_POCKET
|
||||
// =============================================================================
|
||||
// Power saving — adapted from MeshCore PR #2286 (IoTThinks)
|
||||
// Returns true if the radio has outbound packets queued (any priority, any
|
||||
// scheduling window). main.cpp loop() uses this to decide whether it's safe
|
||||
// to drop into board.sleep(0) until the next interrupt.
|
||||
//
|
||||
// Upstream uses _mgr->getOutboundTotal() which doesn't exist in this tree —
|
||||
// the equivalent call in Meck is getOutboundCount(0xFFFFFFFF) which passes
|
||||
// max uint32 as `now` so scheduled_for < now is always true. Already used
|
||||
// elsewhere in this file (see line ~2221 in the queue-stats block).
|
||||
// =============================================================================
|
||||
bool MyMesh::hasPendingWork() const {
|
||||
return _mgr->getOutboundCount(0xFFFFFFFF) > 0;
|
||||
}
|
||||
#endif
|
||||
@@ -8,7 +8,7 @@
|
||||
#define FIRMWARE_VER_CODE 11
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "12 April 2026"
|
||||
#define FIRMWARE_BUILD_DATE "16 April 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
@@ -171,6 +171,14 @@ public:
|
||||
bool setCustomPath(int contactIdx, const uint8_t* path, uint8_t pathLen, bool lock);
|
||||
void clearCustomPath(int contactIdx);
|
||||
|
||||
#ifdef HELTEC_MESH_POCKET
|
||||
// Power saving: check if there is pending work (outbound packets queued, etc.)
|
||||
// Used by main.cpp loop to decide whether board.sleep() is safe.
|
||||
// Adapted from MeshCore PR #2286 (IoTThinks) — substitutes getOutboundCount(0xFFFFFFFF)
|
||||
// for upstream's getOutboundTotal() which doesn't exist in this tree.
|
||||
bool hasPendingWork() const;
|
||||
#endif
|
||||
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override;
|
||||
@@ -309,8 +317,17 @@ private:
|
||||
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
|
||||
int next_ack_idx;
|
||||
|
||||
// Advert path table: stores paths we've heard back to us for sorting/recency.
|
||||
// ESP32 variants (T-Deck Pro, T5S3, Heltec V4) have PSRAM, so can afford the
|
||||
// large 1000-entry table (~50KB). nRF52 companion builds (Heltec Meshpocket,
|
||||
// T-Echo Card) have no PSRAM and only 256KB total SRAM shared with BLE, so
|
||||
// use a much smaller table sized for realistic handheld usage.
|
||||
#if defined(ESP32)
|
||||
#define ADVERT_PATH_TABLE_SIZE 1000
|
||||
AdvertPath* advert_paths; // PSRAM-allocated in begin(), size = ADVERT_PATH_TABLE_SIZE
|
||||
#else
|
||||
#define ADVERT_PATH_TABLE_SIZE 50
|
||||
#endif
|
||||
AdvertPath* advert_paths; // PSRAM-allocated (ESP32) or heap-allocated (nRF52) in begin()
|
||||
|
||||
// Sent message repeat tracking
|
||||
#define SENT_TRACK_SIZE 4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#ifdef BLE_PIN_CODE
|
||||
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
|
||||
#if defined(BLE_PIN_CODE) && defined(ESP32)
|
||||
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi) — ESP32 only
|
||||
#endif
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
#include <esp_ota_ops.h>
|
||||
@@ -11,19 +11,25 @@
|
||||
#include "target.h" // For sensors, board, etc.
|
||||
#include "CPUPowerManager.h"
|
||||
|
||||
// Core screens used by every Meck variant, regardless of display or keyboard
|
||||
// hardware. Kept unconditional here so main.cpp can reference the types on
|
||||
// any build (Meshpocket, Heltec V4, T-Deck Pro, T5S3). SD-dependent screens
|
||||
// (NotesScreen, TextReaderScreen) are header-stubbed for non-ESP32 so this
|
||||
// block is safe on nRF52 too.
|
||||
#include "ContactsScreen.h"
|
||||
#include "ChannelScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "TextReaderScreen.h"
|
||||
|
||||
// T-Deck Pro Keyboard support
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
#include "TCA8418Keyboard.h"
|
||||
#include <SD.h>
|
||||
#include "TextReaderScreen.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
#include "ChannelScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
#ifdef MECK_WEB_READER
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
@@ -679,15 +685,6 @@
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#include "TouchDrvGT911.hpp"
|
||||
#include <SD.h>
|
||||
#include "TextReaderScreen.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
#include "ChannelScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
|
||||
static TouchDrvGT911 gt911Touch;
|
||||
static bool gt911Ready = false;
|
||||
@@ -1675,6 +1672,7 @@ void setup() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
DisplayDriver* disp = NULL;
|
||||
MESH_DEBUG_PRINTLN("setup() - about to call display.begin()");
|
||||
Serial.println("[DIAG] about to call display.begin()");
|
||||
|
||||
// =========================================================================
|
||||
// T-Deck Pro V1.1: Initialize E-Ink reset pin BEFORE display.begin()
|
||||
@@ -1702,16 +1700,21 @@ void setup() {
|
||||
|
||||
if (display.begin()) {
|
||||
MESH_DEBUG_PRINTLN("setup() - display.begin() returned true");
|
||||
Serial.println("[DIAG] display.begin() returned TRUE");
|
||||
disp = &display;
|
||||
disp->startFrame();
|
||||
Serial.println("[DIAG] startFrame() done");
|
||||
#ifdef ST7789
|
||||
disp->setTextSize(2);
|
||||
#endif
|
||||
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
|
||||
Serial.println("[DIAG] Loading text drawn");
|
||||
disp->endFrame();
|
||||
Serial.println("[DIAG] endFrame() done — should show on screen now");
|
||||
MESH_DEBUG_PRINTLN("setup() - Loading screen drawn");
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("setup() - display.begin() returned false!");
|
||||
Serial.println("[DIAG] display.begin() returned FALSE — display NOT initialized");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2268,8 +2271,15 @@ void setup() {
|
||||
the_mesh.setVoiceEnvelopeHandler(voiceEnvelopeCallback);
|
||||
#endif
|
||||
|
||||
Serial.printf("setup() complete — free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
#ifdef ESP32
|
||||
Serial.printf("setup() complete - free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
#else
|
||||
// nRF52 has no ESP.xxx API; dbgMemInfo() prints mallinfo-based heap stats
|
||||
// on Arduino-nRF52 core (dbgMemInfo() is declared in <Adafruit_TinyUSB.h>
|
||||
// via rtos_support.h). Not available on all nRF52 cores so guard further.
|
||||
Serial.println("setup() complete");
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
|
||||
}
|
||||
|
||||
@@ -2800,7 +2810,10 @@ void loop() {
|
||||
#endif
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
// Periodic AGC reset - re-assert boosted RX gain to prevent sensitivity drift
|
||||
#ifdef ESP32
|
||||
// Periodic AGC reset - re-assert boosted RX gain to prevent sensitivity drift.
|
||||
// radio_reset_agc() is only defined in ESP32 variants' target.cpp. nRF52
|
||||
// Meshpocket uses a different radio driver that manages AGC internally.
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
if (!otaRadioPaused)
|
||||
#endif
|
||||
@@ -2808,6 +2821,7 @@ void loop() {
|
||||
radio_reset_agc();
|
||||
lastAGCReset = millis();
|
||||
}
|
||||
#endif
|
||||
// Handle T-Deck Pro keyboard input
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
handleKeyboardInput();
|
||||
@@ -3320,6 +3334,29 @@ void loop() {
|
||||
delay(50);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_POCKET
|
||||
// Power saving — DISABLED for now (April 2026).
|
||||
//
|
||||
// The sd_app_evt_wait() primitive inside HeltecMeshPocket::sleep() blocks
|
||||
// indefinitely waiting for a SoftDevice event. Without BLE activity and
|
||||
// without GPIO SENSE configured on the USER button, the device can get
|
||||
// stuck here — UI render cycles never run, e-ink keeps showing stale
|
||||
// content from before the flash.
|
||||
//
|
||||
// Re-enabling requires either:
|
||||
// (a) extending hasPendingWork() to include pending UI render work, or
|
||||
// (b) configuring PIN_USER_BTN with GPIO SENSE so button presses wake
|
||||
// the SoftDevice, and adding a timed RTC wake (e.g. 50ms) so UI
|
||||
// refresh still happens while idle.
|
||||
//
|
||||
// For now we leave the main loop free-running. DCDC converter alone
|
||||
// (enabled via NRF52BoardDCDC::begin()) still provides meaningful power
|
||||
// savings vs the LDO baseline.
|
||||
// if (!the_mesh.hasPendingWork()) {
|
||||
// board.sleep(0);
|
||||
// }
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -50,7 +50,14 @@ class LastHeardScreen : public UIScreen {
|
||||
public:
|
||||
LastHeardScreen(mesh::RTCClock* rtc)
|
||||
: _rtc(rtc), _scrollPos(0), _count(0) {
|
||||
#if defined(ESP32)
|
||||
// ESP32 variants have PSRAM — allocate the entries buffer there
|
||||
_entries = (AdvertPath*)ps_calloc(LAST_HEARD_DISPLAY_SIZE, sizeof(AdvertPath));
|
||||
#else
|
||||
// nRF52 has no PSRAM — fall back to regular heap. At 100 entries × ~84
|
||||
// bytes each this is ~8.4KB, manageable within Meshpocket's SRAM budget.
|
||||
_entries = (AdvertPath*)calloc(LAST_HEARD_DISPLAY_SIZE, sizeof(AdvertPath));
|
||||
#endif
|
||||
}
|
||||
|
||||
void resetScroll() { _scrollPos = 0; }
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#ifdef ESP32
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#endif
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
|
||||
#ifdef ESP32
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
@@ -1368,4 +1372,42 @@ public:
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
#else // !ESP32
|
||||
|
||||
// Non-ESP32 stub: Meshpocket / T-Echo Card have no SD card hardware, so the
|
||||
// full notes editor (which depends on SD.h) can't work here. This stub keeps
|
||||
// UITask.cpp compilable by providing the same public interface as no-ops.
|
||||
// Navigating to notes from the home screen on a Meshpocket will just render
|
||||
// a placeholder message and do nothing.
|
||||
class NotesScreen : public UIScreen {
|
||||
public:
|
||||
typedef uint32_t (*TimeGetterFn)();
|
||||
|
||||
NotesScreen(UITask* task, NodePrefs* prefs = nullptr) {
|
||||
(void)task; (void)prefs;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 20);
|
||||
display.print("Notes: SD card required");
|
||||
display.setCursor(0, 30);
|
||||
display.print("(not available)");
|
||||
return 5000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override { (void)c; return false; }
|
||||
bool isEditing() const { return false; }
|
||||
void triggerSaveAndExit() {}
|
||||
void exitNotes() {}
|
||||
void enter(DisplayDriver& display) { (void)display; }
|
||||
void setTimestamp(uint32_t rtcTime, int8_t utcOffset) {
|
||||
(void)rtcTime; (void)utcOffset;
|
||||
}
|
||||
void setTimeGetter(TimeGetterFn fn) { (void)fn; }
|
||||
};
|
||||
|
||||
#endif // ESP32
|
||||
@@ -2714,9 +2714,11 @@ public:
|
||||
} else if (type == ROW_GPS_BAUD) {
|
||||
_editPickerIdx--;
|
||||
if (_editPickerIdx < 0) _editPickerIdx = GPS_BAUD_OPTION_COUNT - 1;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
|
||||
} else if (type == ROW_AUTO_LOCK) {
|
||||
_editPickerIdx--;
|
||||
if (_editPickerIdx < 0) _editPickerIdx = AUTO_LOCK_OPTION_COUNT - 1;
|
||||
#endif
|
||||
} else {
|
||||
// Radio preset
|
||||
_editPickerIdx--;
|
||||
@@ -2731,9 +2733,11 @@ public:
|
||||
} else if (type == ROW_GPS_BAUD) {
|
||||
_editPickerIdx++;
|
||||
if (_editPickerIdx >= GPS_BAUD_OPTION_COUNT) _editPickerIdx = 0;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
|
||||
} else if (type == ROW_AUTO_LOCK) {
|
||||
_editPickerIdx++;
|
||||
if (_editPickerIdx >= AUTO_LOCK_OPTION_COUNT) _editPickerIdx = 0;
|
||||
#endif
|
||||
} else {
|
||||
// Radio preset
|
||||
_editPickerIdx++;
|
||||
@@ -2751,12 +2755,14 @@ public:
|
||||
_editMode = EDIT_NONE;
|
||||
Serial.printf("Settings: GPS baud set to %lu (reboot to apply)\n",
|
||||
(unsigned long)_prefs->gps_baudrate);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
|
||||
} else if (type == ROW_AUTO_LOCK) {
|
||||
_prefs->auto_lock_minutes = AUTO_LOCK_OPTIONS[_editPickerIdx];
|
||||
the_mesh.savePrefs();
|
||||
_editMode = EDIT_NONE;
|
||||
Serial.printf("Settings: Auto lock = %s\n",
|
||||
autoLockLabel(_prefs->auto_lock_minutes));
|
||||
#endif
|
||||
} else {
|
||||
// Apply radio preset
|
||||
if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) {
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#include "EpubProcessor.h"
|
||||
#ifdef ESP32
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#include "EpubProcessor.h"
|
||||
#endif
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
|
||||
#ifdef ESP32
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
@@ -1948,4 +1952,42 @@ public:
|
||||
if (_fileOpen) closeBook();
|
||||
_mode = FILE_LIST;
|
||||
}
|
||||
};
|
||||
};
|
||||
#else // !ESP32
|
||||
|
||||
// Non-ESP32 stub: Meshpocket / T-Echo Card have no SD card hardware, so the
|
||||
// full EPUB/text reader can't work here. This stub keeps UITask.cpp and
|
||||
// main.cpp compilable by providing the same public interface as no-ops.
|
||||
// Navigating to the reader on a non-SD board just shows a placeholder.
|
||||
class TextReaderScreen : public UIScreen {
|
||||
public:
|
||||
TextReaderScreen(UITask* task, NodePrefs* prefs = nullptr) {
|
||||
(void)task; (void)prefs;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 20);
|
||||
display.print("Reader: SD card required");
|
||||
display.setCursor(0, 30);
|
||||
display.print("(not available)");
|
||||
return 5000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override { (void)c; return false; }
|
||||
|
||||
// No-op public API matching the ESP32 class for call-site compatibility
|
||||
void invalidateLayout() {}
|
||||
void bootIndex(DisplayDriver& display) { (void)display; }
|
||||
void setSDReady(bool ready) { (void)ready; }
|
||||
void enter(DisplayDriver& display) { (void)display; }
|
||||
bool isReading() const { return false; }
|
||||
bool isInFileList() const { return false; }
|
||||
void gotoPage(int pageNum) { (void)pageNum; }
|
||||
int getTotalPages() const { return 0; }
|
||||
int selectRowAtVY(int vy) { (void)vy; return -1; }
|
||||
void exitReader() {}
|
||||
};
|
||||
|
||||
#endif // ESP32
|
||||
@@ -57,6 +57,9 @@
|
||||
#include "ContactsScreen.h"
|
||||
#include "TextReaderScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
#include "MorseScreen.h"
|
||||
#endif
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
#include "AudiobookPlayerScreen.h"
|
||||
#include "VoiceMessageScreen.h"
|
||||
@@ -66,6 +69,11 @@
|
||||
#include "ModemManager.h"
|
||||
#endif
|
||||
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
// File-scope screen pointer — avoids touching UITask.h, feature is purely optional.
|
||||
static MorseScreen* morse_screen = nullptr;
|
||||
#endif
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
unsigned long dismiss_after;
|
||||
@@ -1283,7 +1291,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
#ifndef HELTEC_MESH_POCKET
|
||||
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
|
||||
#endif
|
||||
channel_screen = new ChannelScreen(this, &rtc_clock);
|
||||
((ChannelScreen*)channel_screen)->setDMUnreadPtr(_dmUnread);
|
||||
contacts_screen = new ContactsScreen(this, &rtc_clock);
|
||||
@@ -1312,6 +1322,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
map_screen = nullptr;
|
||||
#endif
|
||||
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
morse_screen = new MorseScreen(&rtc_clock);
|
||||
#endif
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// Apply saved display preferences before first render
|
||||
if (_node_prefs->portrait_mode) {
|
||||
@@ -1393,9 +1407,11 @@ switch(t){
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
#ifndef HELTEC_MESH_POCKET
|
||||
if (msgcount == 0 && curr == msg_preview) {
|
||||
gotoHomeScreen();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount,
|
||||
@@ -1421,7 +1437,9 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
_dedupIdx = (_dedupIdx + 1) % MSG_DEDUP_SIZE;
|
||||
|
||||
// Add to preview screen (for notifications on non-keyboard devices)
|
||||
#ifndef HELTEC_MESH_POCKET
|
||||
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
|
||||
#endif
|
||||
|
||||
// Determine channel index by looking up the channel name
|
||||
// For channel messages, from_name is the channel name
|
||||
@@ -1464,6 +1482,13 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
}
|
||||
} else {
|
||||
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr);
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
// Mirror Public channel (index 0) messages into the Morse inbox ring.
|
||||
// MorseScreen keeps its own small buffer so it doesn't reach into ChannelScreen.
|
||||
if (channel_idx == 0 && morse_screen != nullptr) {
|
||||
morse_screen->notifyPublicMsg(from_name, text);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// If user is currently viewing this channel, mark it as read immediately
|
||||
@@ -1485,7 +1510,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(LilyGo_TDeck_Pro) || defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#if defined(HELTEC_MESH_POCKET)
|
||||
// Meshpocket: silent — no popup, no toast. Messages are still stored in
|
||||
// channel history (and picked up by MorseScreen's inbox if enabled).
|
||||
#elif defined(LilyGo_TDeck_Pro) || defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// Don't interrupt user with popup - just show brief notification
|
||||
// Messages are stored in channel history, accessible via tile/key
|
||||
// Suppress toasts for room server messages (bulk sync would spam toasts)
|
||||
@@ -1644,6 +1672,13 @@ void UITask::loop() {
|
||||
}
|
||||
#elif defined(PIN_USER_BTN)
|
||||
int ev = user_btn.check();
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
// While MorseScreen is active, it reads the button directly via poll().
|
||||
// Swallow all click/long-press events so they don't fire nav actions.
|
||||
if (morse_screen != nullptr && curr == morse_screen) {
|
||||
ev = BUTTON_EVENT_NONE;
|
||||
}
|
||||
#endif
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// T5S3: single click = cycle pages on home, go back to home from elsewhere
|
||||
@@ -1806,6 +1841,37 @@ void UITask::loop() {
|
||||
|
||||
if (curr) curr->poll();
|
||||
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
// When MorseScreen is active, poll its cross-screen flags.
|
||||
if (morse_screen != nullptr && curr == morse_screen) {
|
||||
// 1. Send request (AR prosign) — dispatch to Public channel (index 0)
|
||||
const char* morseText = nullptr;
|
||||
if (morse_screen->consumeSendRequest(&morseText) && morseText && morseText[0]) {
|
||||
ChannelDetails channel;
|
||||
if (the_mesh.getChannel(0, channel)) {
|
||||
uint32_t timestamp = rtc_clock.getCurrentTime();
|
||||
int textLen = (int)strlen(morseText);
|
||||
const char* sender = the_mesh.getNodePrefs()->node_name;
|
||||
if (the_mesh.sendGroupMessage(timestamp, channel.channel, sender, morseText, textLen)) {
|
||||
addSentChannelMessage(0, sender, morseText);
|
||||
the_mesh.queueSentChannelMessage(0, timestamp, sender, morseText);
|
||||
showAlert("Sent!", 1200);
|
||||
morse_screen->clearOutBuf();
|
||||
} else {
|
||||
showAlert("Send failed", 1500);
|
||||
}
|
||||
} else {
|
||||
showAlert("No Public ch", 1500);
|
||||
}
|
||||
}
|
||||
// 2. Exit gesture (long-hold) — return to home
|
||||
if (morse_screen->wantsExit()) {
|
||||
morse_screen->acknowledgeExit();
|
||||
gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_display != NULL && _display->isOn()) {
|
||||
if (millis() >= _next_refresh && curr) {
|
||||
// Sync dark mode with prefs (settings toggle takes effect here)
|
||||
@@ -2142,8 +2208,20 @@ char UITask::handleTripleClick(char c) {
|
||||
board.setBacklightBrightness(4);
|
||||
board.setBacklight(true);
|
||||
}
|
||||
#else
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
// Triple-click from home screen → enter Morse compose mode.
|
||||
// From any other screen, fall through to the existing buzzer toggle (no-op
|
||||
// on Meshpocket but kept for other single-button variants).
|
||||
if (morse_screen != nullptr && curr == home) {
|
||||
morse_screen->activate();
|
||||
setCurrScreen(morse_screen);
|
||||
} else {
|
||||
toggleBuzzer();
|
||||
}
|
||||
#else
|
||||
toggleBuzzer();
|
||||
#endif
|
||||
#endif
|
||||
c = 0;
|
||||
return c;
|
||||
|
||||
@@ -79,7 +79,9 @@ class UITask : public AbstractUITask {
|
||||
|
||||
UIScreen* splash;
|
||||
UIScreen* home;
|
||||
#ifndef HELTEC_MESH_POCKET
|
||||
UIScreen* msg_preview;
|
||||
#endif
|
||||
UIScreen* channel_screen; // Channel message history screen
|
||||
UIScreen* contacts_screen; // Contacts list screen
|
||||
UIScreen* text_reader; // *** NEW: Text reader screen ***
|
||||
@@ -306,7 +308,9 @@ public:
|
||||
|
||||
// Get current screen for checking state
|
||||
UIScreen* getCurrentScreen() const { return curr; }
|
||||
#ifndef HELTEC_MESH_POCKET
|
||||
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
|
||||
#endif
|
||||
UIScreen* getTextReaderScreen() const { return text_reader; } // *** NEW ***
|
||||
UIScreen* getNotesScreen() const { return notes_screen; }
|
||||
UIScreen* getContactsScreen() const { return contacts_screen; }
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// CPU Frequency Scaling for ESP32-S3
|
||||
//
|
||||
// Typical current draw (CPU only, rough):
|
||||
// 240 MHz ~70-80 mA
|
||||
// 160 MHz ~50-60 mA
|
||||
// 80 MHz ~30-40 mA
|
||||
// 40 MHz ~15-20 mA (low-power / lock screen mode)
|
||||
//
|
||||
// SPI peripherals and UART use their own clock dividers from the APB clock,
|
||||
// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz.
|
||||
|
||||
#ifdef ESP32
|
||||
|
||||
#ifndef CPU_FREQ_IDLE
|
||||
#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening
|
||||
#endif
|
||||
|
||||
#ifndef CPU_FREQ_BOOST
|
||||
#define CPU_FREQ_BOOST 240 // MHz — heavy processing
|
||||
#endif
|
||||
|
||||
#ifndef CPU_FREQ_LOW_POWER
|
||||
#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C)
|
||||
#endif
|
||||
|
||||
#ifndef CPU_BOOST_TIMEOUT_MS
|
||||
#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds
|
||||
#endif
|
||||
|
||||
class CPUPowerManager {
|
||||
public:
|
||||
CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {}
|
||||
|
||||
void begin() {
|
||||
setCpuFrequencyMhz(CPU_FREQ_IDLE);
|
||||
_boosted = false;
|
||||
_lowPower = false;
|
||||
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) {
|
||||
// Return to low-power if locked, otherwise normal idle
|
||||
if (_lowPower) {
|
||||
setCpuFrequencyMhz(CPU_FREQ_LOW_POWER);
|
||||
MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER);
|
||||
} else {
|
||||
setCpuFrequencyMhz(CPU_FREQ_IDLE);
|
||||
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
|
||||
}
|
||||
_boosted = false;
|
||||
}
|
||||
}
|
||||
|
||||
void setBoost() {
|
||||
if (!_boosted) {
|
||||
setCpuFrequencyMhz(CPU_FREQ_BOOST);
|
||||
_boosted = true;
|
||||
MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST);
|
||||
}
|
||||
_boost_started = millis();
|
||||
}
|
||||
|
||||
void setIdle() {
|
||||
if (_boosted) {
|
||||
setCpuFrequencyMhz(CPU_FREQ_IDLE);
|
||||
_boosted = false;
|
||||
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
|
||||
}
|
||||
if (_lowPower) {
|
||||
_lowPower = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Low-power mode — drops CPU to 40 MHz for lock screen standby.
|
||||
// If currently boosted, the boost timeout will return to 40 MHz
|
||||
// instead of 80 MHz.
|
||||
void setLowPower() {
|
||||
_lowPower = true;
|
||||
if (!_boosted) {
|
||||
setCpuFrequencyMhz(CPU_FREQ_LOW_POWER);
|
||||
MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER);
|
||||
}
|
||||
// If boosted, the loop() timeout will drop to low-power instead of idle
|
||||
}
|
||||
|
||||
// Exit low-power mode — returns to normal idle (80 MHz).
|
||||
// If currently boosted, the boost timeout will return to idle
|
||||
// instead of low-power.
|
||||
void clearLowPower() {
|
||||
_lowPower = false;
|
||||
if (!_boosted) {
|
||||
setCpuFrequencyMhz(CPU_FREQ_IDLE);
|
||||
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE);
|
||||
}
|
||||
// If boosted, the loop() timeout will drop to idle as normal
|
||||
}
|
||||
|
||||
bool isBoosted() const { return _boosted; }
|
||||
bool isLowPower() const { return _lowPower; }
|
||||
uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); }
|
||||
|
||||
private:
|
||||
bool _boosted;
|
||||
bool _lowPower;
|
||||
unsigned long _boost_started;
|
||||
};
|
||||
|
||||
#else // !ESP32
|
||||
|
||||
// Non-ESP32 stub: same public interface, all methods no-op. Keeps main.cpp
|
||||
// compilable without scattering #ifdef guards around every cpuPower.xxx()
|
||||
// call. Platform-specific power saving on nRF52 is handled separately via
|
||||
// HeltecMeshPocket::sleep() and the MyMesh::hasPendingWork() check in loop().
|
||||
class CPUPowerManager {
|
||||
public:
|
||||
CPUPowerManager() {}
|
||||
void begin() {}
|
||||
void loop() {}
|
||||
void setBoost() {}
|
||||
void setIdle() {}
|
||||
void setLowPower() {}
|
||||
void clearLowPower() {}
|
||||
bool isBoosted() const { return false; }
|
||||
bool isLowPower() const { return false; }
|
||||
uint32_t getFrequencyMHz() const { return 0; }
|
||||
};
|
||||
|
||||
#endif // ESP32
|
||||
@@ -0,0 +1,55 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <nrf_soc.h>
|
||||
|
||||
#include "MeshPocket.h"
|
||||
|
||||
void HeltecMeshPocket::begin() {
|
||||
// Call NRF52BoardDCDC::begin() rather than NRF52Board::begin() so the
|
||||
// internal DC/DC regulator is actually enabled — this is the whole reason
|
||||
// HeltecMeshPocket extends NRF52BoardDCDC and directly improves battery
|
||||
// life by ~30% during BLE transmit bursts vs the default LDO.
|
||||
NRF52BoardDCDC::begin();
|
||||
Serial.begin(115200);
|
||||
pinMode(PIN_VBAT_READ, INPUT);
|
||||
|
||||
pinMode(PIN_USER_BTN, INPUT);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Power saving — CPU light-sleep until next interrupt.
|
||||
// Adapted from MeshCore PR #1353 (IoTThinks) with the "safe to sleep" check
|
||||
// pattern suggested by fschrempf in the review. Called from main.cpp loop()
|
||||
// only when hasPendingWork() returns false.
|
||||
//
|
||||
// Wakeup sources (no GPIO sense configuration needed — RadioLib already
|
||||
// attaches a DIO1 IRQ, MomentaryButton is polled from loop so any GPIO
|
||||
// change via attachInterrupt or the RTC1 tick wakes us):
|
||||
// - LoRa DIO1 (incoming packet / TX done)
|
||||
// - RTC1 tick (~1ms, drives millis() and scheduler)
|
||||
// - USER button (via RTC tick on next poll, or attached IRQ if added)
|
||||
// - SoftDevice (BLE stack events)
|
||||
//
|
||||
// When BLE SoftDevice is active we MUST use sd_app_evt_wait() rather than
|
||||
// raw __WFE() — calling WFE directly with SoftDevice enabled can wedge the
|
||||
// BLE stack. When it's not active (USB-only builds, or BLE disabled via
|
||||
// settings) we use WFE directly.
|
||||
// =============================================================================
|
||||
void HeltecMeshPocket::sleep(uint32_t secs) {
|
||||
(void)secs; // NRF52 ignores — any interrupt wakes us
|
||||
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
|
||||
if (sd_enabled) {
|
||||
// BLE is active (includes OTA) — use SoftDevice primitive.
|
||||
// This is the only safe way to sleep while the SoftDevice is running.
|
||||
sd_app_evt_wait();
|
||||
} else {
|
||||
// No SoftDevice — raw ARM WFE. Double-WFE pattern clears any stale
|
||||
// event flag on the first call and actually sleeps on the second.
|
||||
__SEV();
|
||||
__WFE(); // clear event flag
|
||||
__WFE(); // sleep until next event
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
|
||||
// built-ins
|
||||
#define PIN_VBAT_READ 29
|
||||
#define PIN_BAT_CTL 34
|
||||
#define MV_LSB (3000.0F / 4096.0F) // 12-bit ADC with 3.0V input range
|
||||
|
||||
// HeltecMeshPocket inherits from BOTH NRF52BoardDCDC (for DC/DC converter
|
||||
// efficiency) AND NRF52BoardOTA (for BLE OTA firmware update support). Both
|
||||
// parent classes inherit virtually from NRF52Board so there's only one copy
|
||||
// of the base-class state. The NRF52BoardOTA constructor needs the OTA
|
||||
// advertising name; NRF52BoardDCDC has no explicit constructor so uses the
|
||||
// default.
|
||||
class HeltecMeshPocket : public NRF52BoardDCDC, public NRF52BoardOTA {
|
||||
public:
|
||||
// Cast required because NRF52BoardOTA stores the name as non-const char*
|
||||
// (latent const-correctness issue in the shared header). The name is only
|
||||
// read, never modified, so the cast is safe.
|
||||
HeltecMeshPocket() : NRF52BoardOTA((char*)"MESH_POCKET_OTA") {}
|
||||
void begin();
|
||||
|
||||
uint16_t getBattMilliVolts() override {
|
||||
int adcvalue = 0;
|
||||
analogReadResolution(12);
|
||||
analogReference(AR_INTERNAL_3_0);
|
||||
pinMode(PIN_BAT_CTL, OUTPUT); // battery adc can be read only ctrl pin set to high
|
||||
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 * 4.9);
|
||||
}
|
||||
|
||||
const char* getManufacturerName() const override {
|
||||
return "Heltec MeshPocket";
|
||||
}
|
||||
|
||||
void powerOff() override {
|
||||
sd_power_system_off();
|
||||
}
|
||||
|
||||
// Power saving — adapted from MeshCore PR #1353 (IoTThinks).
|
||||
// Puts the nRF52 into CPU light-sleep until any interrupt fires (LoRa DIO1,
|
||||
// USER button, RTC tick, SoftDevice event). When BLE is live (e.g. OTA in
|
||||
// progress or companion app connected) we fall through to a plain event wait
|
||||
// rather than the SoftDevice primitive — this is the "safe to sleep" pattern
|
||||
// from the PR review discussion that avoids BLE stack deadlocks.
|
||||
//
|
||||
// The `secs` param is ignored on NRF52 (matches upstream PR #2286 usage:
|
||||
// main.cpp passes 0 meaning "sleep whenever possible"). Any enabled IRQ
|
||||
// wakes the CPU — the RTC1 tick (~1ms) provides a hard ceiling on wake
|
||||
// latency, keeping MorseScreen timing responsive.
|
||||
void sleep(uint32_t secs) override;
|
||||
};
|
||||
@@ -0,0 +1,413 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// MorseScreen — single-button Morse compose/receive for the Meshpocket
|
||||
//
|
||||
// Entered from the home screen via a triple-click on the USER button when
|
||||
// MORSE_COMPOSE_ENABLED is defined (Meshpocket companion builds only).
|
||||
//
|
||||
// While active, this screen takes exclusive ownership of the USER button:
|
||||
// - Short press -> dot (<240 ms by default)
|
||||
// - Longer press -> dash (>=240 ms)
|
||||
// - Letter gap (~360 ms silence) commits the staged pattern to the buffer
|
||||
// - Word gap (~840 ms silence) inserts a space
|
||||
// - `AR` prosign (.-.-.) -> send to Public (channel 0), clear buffer
|
||||
// - `HH` prosign (........) -> backspace one character
|
||||
// - 5 s continuous hold -> exit back to home screen
|
||||
//
|
||||
// The screen maintains its own tiny ring buffer of the most recent Public
|
||||
// channel messages (populated from UITask::newMsg when channel_idx == 0) so
|
||||
// that it does not need to reach into ChannelScreen internals.
|
||||
//
|
||||
// Sending is delegated to UITask via the consumeSendRequest() flag pattern
|
||||
// so that this header has no dependency on MyMesh / BaseChatMesh types.
|
||||
// =============================================================================
|
||||
|
||||
#ifdef MORSE_COMPOSE_ENABLED
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#include <MeshCore.h>
|
||||
|
||||
// user_btn is instantiated in variants/mesh_pocket/target.cpp
|
||||
extern MomentaryButton user_btn;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tunables
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Standard Morse timing: WPM = 1.2 / dot_seconds
|
||||
// 10 WPM -> dot = 120 ms
|
||||
#define MORSE_DOT_UNIT_MS 120
|
||||
|
||||
// Press shorter than this = dot, longer = dash.
|
||||
// 2x dot is a common midpoint threshold (dash is nominally 3x dot).
|
||||
#define MORSE_DOT_DASH_MS (MORSE_DOT_UNIT_MS * 2)
|
||||
|
||||
// Inter-letter silence that commits the staged pattern (3 dot units).
|
||||
#define MORSE_LETTER_GAP_MS (MORSE_DOT_UNIT_MS * 3)
|
||||
|
||||
// Inter-word silence that inserts a space (7 dot units).
|
||||
#define MORSE_WORD_GAP_MS (MORSE_DOT_UNIT_MS * 7)
|
||||
|
||||
// Exit gesture — longer than any conceivable dash, dominant hand will tire.
|
||||
#define MORSE_EXIT_HOLD_MS 5000
|
||||
|
||||
// Buffer sizes
|
||||
#define MORSE_OUT_BUF_LEN 134 // MeshCore per-channel msg cap is ~133
|
||||
#define MORSE_STAGING_MAX 12 // longest pattern we accept (HH = 8)
|
||||
#define MORSE_INBOX_SIZE 3
|
||||
#define MORSE_INBOX_TEXT_LEN 96
|
||||
#define MORSE_INBOX_NAME_LEN 32
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Morse lookup — ITU minimal + basic punctuation
|
||||
// Stored in flash; tiny (~400 bytes). RAM impact: zero.
|
||||
// -----------------------------------------------------------------------------
|
||||
struct MorseEntry {
|
||||
char c;
|
||||
const char* pat;
|
||||
};
|
||||
|
||||
static const MorseEntry MORSE_TABLE[] = {
|
||||
{'A', ".-"}, {'B', "-..."}, {'C', "-.-."}, {'D', "-.."},
|
||||
{'E', "."}, {'F', "..-."}, {'G', "--."}, {'H', "...."},
|
||||
{'I', ".."}, {'J', ".---"}, {'K', "-.-"}, {'L', ".-.."},
|
||||
{'M', "--"}, {'N', "-."}, {'O', "---"}, {'P', ".--."},
|
||||
{'Q', "--.-"}, {'R', ".-."}, {'S', "..."}, {'T', "-"},
|
||||
{'U', "..-"}, {'V', "...-"}, {'W', ".--"}, {'X', "-..-"},
|
||||
{'Y', "-.--"}, {'Z', "--.."},
|
||||
{'0', "-----"}, {'1', ".----"}, {'2', "..---"}, {'3', "...--"},
|
||||
{'4', "....-"}, {'5', "....."}, {'6', "-...."}, {'7', "--..."},
|
||||
{'8', "---.."}, {'9', "----."},
|
||||
{'.', ".-.-.-"},{',', "--..--"},{'?', "..--.."},
|
||||
{0, nullptr}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
class MorseScreen : public UIScreen {
|
||||
mesh::RTCClock* _rtc;
|
||||
|
||||
// Outgoing composition
|
||||
char _outBuf[MORSE_OUT_BUF_LEN];
|
||||
uint16_t _outLen;
|
||||
|
||||
// Current letter staging (dots/dashes not yet decoded)
|
||||
char _staging[MORSE_STAGING_MAX];
|
||||
uint8_t _stagingLen;
|
||||
|
||||
// Key timing state
|
||||
bool _btnPrevPressed;
|
||||
unsigned long _pressStart;
|
||||
unsigned long _releaseAt; // 0 if not yet released after last press
|
||||
bool _letterDecoded; // set after commitStaging() — awaits word gap
|
||||
bool _wordSpaceInserted;
|
||||
bool _exitArmed; // hold threshold crossed; exits on release
|
||||
|
||||
// Cross-screen requests (UITask polls these)
|
||||
bool _wantsExit;
|
||||
bool _wantsSend;
|
||||
|
||||
// Incoming ring buffer — channel 0 (Public) only
|
||||
struct InboxEntry {
|
||||
uint32_t timestamp;
|
||||
char from[MORSE_INBOX_NAME_LEN];
|
||||
char text[MORSE_INBOX_TEXT_LEN];
|
||||
bool valid;
|
||||
};
|
||||
InboxEntry _inbox[MORSE_INBOX_SIZE];
|
||||
uint8_t _inboxNewest; // index of most recent entry
|
||||
uint8_t _inboxCount;
|
||||
|
||||
bool _dirty;
|
||||
unsigned long _nextRender;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Morse decode
|
||||
// Returns the ASCII character for a pattern, or:
|
||||
// '\x01' = AR prosign ".-.-." (send)
|
||||
// '\x02' = HH prosign "........" (backspace)
|
||||
// 0 = no match (silently drop)
|
||||
// ---------------------------------------------------------------------------
|
||||
char decodeStaging() const {
|
||||
if (_stagingLen == 0) return 0;
|
||||
if (strcmp(_staging, ".-.-.") == 0) return '\x01';
|
||||
if (strcmp(_staging, "........") == 0) return '\x02';
|
||||
for (const MorseEntry* e = MORSE_TABLE; e->c != 0; e++) {
|
||||
if (strcmp(_staging, e->pat) == 0) return e->c;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void commitStaging() {
|
||||
if (_stagingLen == 0) return;
|
||||
char decoded = decodeStaging();
|
||||
if (decoded == '\x01') {
|
||||
// AR — request send from UITask
|
||||
if (_outLen > 0) _wantsSend = true;
|
||||
} else if (decoded == '\x02') {
|
||||
// HH — backspace one character (skip trailing space if present)
|
||||
if (_outLen > 0) {
|
||||
_outLen--;
|
||||
_outBuf[_outLen] = 0;
|
||||
}
|
||||
} else if (decoded != 0) {
|
||||
if (_outLen < MORSE_OUT_BUF_LEN - 1) {
|
||||
_outBuf[_outLen++] = decoded;
|
||||
_outBuf[_outLen] = 0;
|
||||
}
|
||||
}
|
||||
_stagingLen = 0;
|
||||
_staging[0] = 0;
|
||||
_letterDecoded = true;
|
||||
_wordSpaceInserted = false;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
void insertWordSpace() {
|
||||
if (_outLen > 0 && _outBuf[_outLen - 1] != ' '
|
||||
&& _outLen < MORSE_OUT_BUF_LEN - 1) {
|
||||
_outBuf[_outLen++] = ' ';
|
||||
_outBuf[_outLen] = 0;
|
||||
_dirty = true;
|
||||
}
|
||||
_wordSpaceInserted = true;
|
||||
}
|
||||
|
||||
public:
|
||||
MorseScreen(mesh::RTCClock* rtc)
|
||||
: _rtc(rtc),
|
||||
_outLen(0), _stagingLen(0),
|
||||
_btnPrevPressed(false), _pressStart(0), _releaseAt(0),
|
||||
_letterDecoded(false), _wordSpaceInserted(false), _exitArmed(false),
|
||||
_wantsExit(false), _wantsSend(false),
|
||||
_inboxNewest(0), _inboxCount(0),
|
||||
_dirty(true), _nextRender(0)
|
||||
{
|
||||
_outBuf[0] = 0;
|
||||
_staging[0] = 0;
|
||||
memset(_inbox, 0, sizeof(_inbox));
|
||||
}
|
||||
|
||||
// Called by UITask when the screen is activated (on triple-click from home)
|
||||
// Resets composition state so each session starts clean.
|
||||
void activate() {
|
||||
_outLen = 0; _outBuf[0] = 0;
|
||||
_stagingLen = 0; _staging[0] = 0;
|
||||
_btnPrevPressed = user_btn.isPressed();
|
||||
_pressStart = 0;
|
||||
_releaseAt = 0;
|
||||
_letterDecoded = false;
|
||||
_wordSpaceInserted = false;
|
||||
_exitArmed = false;
|
||||
_wantsExit = false;
|
||||
_wantsSend = false;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
// Called from UITask::newMsg when channel_idx == 0 (Public).
|
||||
// `from` is the channel name; `text` is the mesh-layer text which already
|
||||
// contains "sender: message" for channel messages.
|
||||
void notifyPublicMsg(const char* from, const char* text) {
|
||||
_inboxNewest = (_inboxCount == 0) ? 0 : ((_inboxNewest + 1) % MORSE_INBOX_SIZE);
|
||||
InboxEntry& e = _inbox[_inboxNewest];
|
||||
e.timestamp = _rtc ? _rtc->getCurrentTime() : 0;
|
||||
if (from) {
|
||||
strncpy(e.from, from, MORSE_INBOX_NAME_LEN - 1);
|
||||
e.from[MORSE_INBOX_NAME_LEN - 1] = 0;
|
||||
} else {
|
||||
e.from[0] = 0;
|
||||
}
|
||||
if (text) {
|
||||
strncpy(e.text, text, MORSE_INBOX_TEXT_LEN - 1);
|
||||
e.text[MORSE_INBOX_TEXT_LEN - 1] = 0;
|
||||
} else {
|
||||
e.text[0] = 0;
|
||||
}
|
||||
e.valid = true;
|
||||
if (_inboxCount < MORSE_INBOX_SIZE) _inboxCount++;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UITask bridges — polled each loop iteration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Returns the outgoing buffer pointer if a send was requested (AR prosign).
|
||||
// Caller clears the buffer via clearOutBuf() after a successful send.
|
||||
bool consumeSendRequest(const char** textOut) {
|
||||
if (!_wantsSend) return false;
|
||||
_wantsSend = false;
|
||||
if (textOut) *textOut = _outBuf;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool wantsExit() const { return _wantsExit; }
|
||||
void acknowledgeExit() { _wantsExit = false; }
|
||||
|
||||
void clearOutBuf() {
|
||||
_outLen = 0;
|
||||
_outBuf[0] = 0;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UIScreen contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void poll() override {
|
||||
unsigned long now = millis();
|
||||
bool pressed = user_btn.isPressed();
|
||||
|
||||
if (pressed && !_btnPrevPressed) {
|
||||
// Edge: released -> pressed
|
||||
_pressStart = now;
|
||||
_exitArmed = false;
|
||||
_letterDecoded = false;
|
||||
_wordSpaceInserted = false;
|
||||
} else if (!pressed && _btnPrevPressed) {
|
||||
// Edge: pressed -> released
|
||||
unsigned long dur = now - _pressStart;
|
||||
if (_exitArmed) {
|
||||
// Exit-hold completed — signal UITask to navigate back to home.
|
||||
// Do NOT add this press to staging.
|
||||
_wantsExit = true;
|
||||
} else {
|
||||
// Normal dot/dash
|
||||
if (_stagingLen < MORSE_STAGING_MAX - 1) {
|
||||
_staging[_stagingLen++] = (dur < MORSE_DOT_DASH_MS) ? '.' : '-';
|
||||
_staging[_stagingLen] = 0;
|
||||
}
|
||||
_releaseAt = now;
|
||||
_dirty = true;
|
||||
}
|
||||
} else if (pressed && _btnPrevPressed) {
|
||||
// Still holding — check for exit-arm threshold
|
||||
if (!_exitArmed && (now - _pressStart) >= MORSE_EXIT_HOLD_MS) {
|
||||
_exitArmed = true;
|
||||
_dirty = true; // redraw to show "release to exit" hint
|
||||
}
|
||||
} else {
|
||||
// Idle (not pressed, wasn't pressed) — check gap timers
|
||||
if (_stagingLen > 0 && _releaseAt > 0
|
||||
&& (now - _releaseAt) >= MORSE_LETTER_GAP_MS) {
|
||||
commitStaging();
|
||||
_releaseAt = now; // reset so word gap measures from commit
|
||||
} else if (_outLen > 0 && _letterDecoded && !_wordSpaceInserted
|
||||
&& _releaseAt > 0
|
||||
&& (now - _releaseAt) >= MORSE_WORD_GAP_MS) {
|
||||
insertWordSpace();
|
||||
}
|
||||
}
|
||||
|
||||
_btnPrevPressed = pressed;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
const int W = display.width();
|
||||
const int H = display.height();
|
||||
|
||||
display.setTextSize(1);
|
||||
|
||||
// ---- Header strip --------------------------------------------------------
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(2, 1);
|
||||
display.print("MORSE \xB7 PUBLIC");
|
||||
|
||||
// Exit hint (right-aligned)
|
||||
display.setColor(_exitArmed ? DisplayDriver::GREEN : DisplayDriver::LIGHT);
|
||||
const char* hint = _exitArmed ? "Release -> exit" : "Hold to exit";
|
||||
display.drawTextRightAlign(W - 2, 1, hint);
|
||||
|
||||
// HR
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, W, 1);
|
||||
|
||||
// ---- Inbox (last N Public messages) --------------------------------------
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(2, 14);
|
||||
display.print("IN");
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_inboxCount == 0) {
|
||||
display.setCursor(22, 14);
|
||||
display.print("(no messages yet)");
|
||||
} else {
|
||||
int y = 14;
|
||||
// Iterate from newest to oldest
|
||||
for (int i = 0; i < _inboxCount && i < MORSE_INBOX_SIZE; i++) {
|
||||
int idx = (int)_inboxNewest - i;
|
||||
while (idx < 0) idx += MORSE_INBOX_SIZE;
|
||||
const InboxEntry& e = _inbox[idx];
|
||||
if (!e.valid) continue;
|
||||
display.drawTextEllipsized(22, y, W - 24, e.text);
|
||||
y += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// HR
|
||||
display.drawRect(0, 48, W, 1);
|
||||
|
||||
// ---- Outgoing buffer -----------------------------------------------------
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(2, 51);
|
||||
display.print("OUT");
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
// Render outgoing with a cursor caret
|
||||
char outWithCursor[MORSE_OUT_BUF_LEN + 2];
|
||||
if (_outLen == 0) {
|
||||
strcpy(outWithCursor, "_");
|
||||
} else {
|
||||
// Show last portion that fits if message is long
|
||||
strncpy(outWithCursor, _outBuf, sizeof(outWithCursor) - 2);
|
||||
outWithCursor[sizeof(outWithCursor) - 2] = 0;
|
||||
size_t n = strlen(outWithCursor);
|
||||
if (n < sizeof(outWithCursor) - 1) {
|
||||
outWithCursor[n] = '_';
|
||||
outWithCursor[n + 1] = 0;
|
||||
}
|
||||
}
|
||||
// Word-wrap inside the strip (y=62..85 approximately)
|
||||
display.setCursor(2, 62);
|
||||
display.printWordWrap(outWithCursor, W - 4);
|
||||
|
||||
// HR
|
||||
display.drawRect(0, 90, W, 1);
|
||||
|
||||
// ---- Staging (current key sequence) --------------------------------------
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(2, 93);
|
||||
display.print("KEY");
|
||||
|
||||
display.setColor(_exitArmed ? DisplayDriver::YELLOW : DisplayDriver::LIGHT);
|
||||
display.setTextSize(2);
|
||||
display.setCursor(30, 93);
|
||||
display.print(_stagingLen > 0 ? _staging : " ");
|
||||
display.setTextSize(1);
|
||||
|
||||
// Character count indicator (bottom-right)
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char ccBuf[12];
|
||||
snprintf(ccBuf, sizeof(ccBuf), "%u/%u", (unsigned)_outLen,
|
||||
(unsigned)(MORSE_OUT_BUF_LEN - 1));
|
||||
display.drawTextRightAlign(W - 2, H - 10, ccBuf);
|
||||
|
||||
// Suppress H-unused warning on builds where width/height differ
|
||||
(void)H;
|
||||
|
||||
_dirty = false;
|
||||
_nextRender = millis();
|
||||
|
||||
// Refresh cadence:
|
||||
// - 200 ms while the user is actively keying (captures staging changes)
|
||||
// - 800 ms otherwise (incoming messages, idle)
|
||||
bool active = (_stagingLen > 0) || _btnPrevPressed || _exitArmed;
|
||||
return active ? 200 : 800;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // MORSE_COMPOSE_ENABLED
|
||||
@@ -0,0 +1,69 @@
|
||||
; ============================================================================
|
||||
; Meck — Heltec MeshPocket variant configuration
|
||||
; ============================================================================
|
||||
; nRF52840 + SX1262 + 2.13" E-Ink (GxEPD2_213_B74)
|
||||
; Single USER button (GPIO 42), no buzzer, no user-accessible LED
|
||||
; ============================================================================
|
||||
|
||||
[Mesh_pocket]
|
||||
extends = nrf52_base
|
||||
board = heltec_mesh_pocket
|
||||
platform_packages = framework-arduinoadafruitnrf52
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
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/mesh_pocket
|
||||
-D HELTEC_MESH_POCKET
|
||||
-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 EINK_DISPLAY_MODEL=GxEPD2_213_B74
|
||||
-D EINK_SCALE_X=1.953125f
|
||||
-D EINK_SCALE_Y=1.28f
|
||||
-D EINK_X_OFFSET=0
|
||||
-D EINK_Y_OFFSET=10
|
||||
-D DISPLAY_CLASS=GxEPDDisplay
|
||||
-D DISABLE_DIAGNOSTIC_OUTPUT
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<../variants/mesh_pocket>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${nrf52_base.lib_deps}
|
||||
adafruit/Adafruit EPD @ 4.6.1
|
||||
rweather/Crypto @ ^0.4.0
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
zinggjm/GxEPD2 @ 1.6.2
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
|
||||
debug_tool = jlink
|
||||
upload_protocol = nrfutil
|
||||
|
||||
[env:Mesh_pocket_companion_radio_ble]
|
||||
extends = Mesh_pocket
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
board_upload.maximum_size = 712704
|
||||
build_flags =
|
||||
${Mesh_pocket.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=500
|
||||
-D MAX_GROUP_CHANNELS=8
|
||||
-D BLE_PIN_CODE=234567
|
||||
-D OFFLINE_QUEUE_SIZE=64
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
-D MORSE_COMPOSE_ENABLED=1
|
||||
; -D BLE_DEBUG_LOGGING=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
build_src_filter = ${Mesh_pocket.build_src_filter}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${Mesh_pocket.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
HeltecMeshPocket 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);
|
||||
|
||||
SensorManager sensors = SensorManager();
|
||||
|
||||
VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#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/SensorManager.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#include "MeshPocket.h"
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/GxEPDDisplay.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
extern HeltecMeshPocket board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
|
||||
#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();
|
||||
|
||||
extern SensorManager sensors;
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "variant.h"
|
||||
#include "nrf.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[] = {
|
||||
// P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
|
||||
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};
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* variant.h
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "WVariant.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Low frequency clock source
|
||||
|
||||
#define USE_LFXO // 32.768 kHz crystal oscillator
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Power
|
||||
|
||||
#define BATTERY_PIN (0 + 29)
|
||||
#define PIN_BAT_CTRL (32 + 2)
|
||||
#define ADC_MULTIPLIER (4.90F)
|
||||
|
||||
#define ADC_RESOLUTION (14)
|
||||
#define BATTERY_SENSE_RES (12)
|
||||
|
||||
#define AREF_VOLTAGE (3.0)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Number of pins
|
||||
|
||||
#define PINS_COUNT (48)
|
||||
#define NUM_DIGITAL_PINS (48)
|
||||
#define NUM_ANALOG_INPUTS (1)
|
||||
#define NUM_ANALOG_OUTPUTS (0)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// UART pin definition
|
||||
|
||||
#define PIN_SERIAL1_RX (37)
|
||||
#define PIN_SERIAL1_TX (39)
|
||||
|
||||
#define PIN_SERIAL2_RX (7)
|
||||
#define PIN_SERIAL2_TX (8)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// I2C pin definition
|
||||
#define WIRE_INTERFACES_COUNT (1)
|
||||
|
||||
#define PIN_WIRE_SDA (32+15)
|
||||
#define PIN_WIRE_SCL (32+13)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Builtin LEDs
|
||||
|
||||
#define LED_BUILTIN (13)
|
||||
#define PIN_LED LED_BUILTIN
|
||||
#define LED_RED LED_BUILTIN
|
||||
#define LED_BLUE (-1) // No blue led, prevents Bluefruit flashing the green LED during advertising
|
||||
#define PIN_STATUS_LED LED_BUILTIN
|
||||
|
||||
#define LED_STATE_ON LOW
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Builtin buttons
|
||||
|
||||
#define PIN_BUTTON1 (32 + 10)
|
||||
#define BUTTON_PIN PIN_BUTTON1
|
||||
|
||||
// #define PIN_BUTTON2 (0 + 18)
|
||||
// #define BUTTON_PIN2 PIN_BUTTON2
|
||||
|
||||
#define PIN_USER_BTN BUTTON_PIN
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// SPI pin definition
|
||||
#define SPI_INTERFACES_COUNT (2)
|
||||
|
||||
// Lora
|
||||
#define USE_SX1262
|
||||
#define SX126X_CS (0 + 26)
|
||||
#define SX126X_DIO1 (0 + 16)
|
||||
#define SX126X_BUSY (0 + 15)
|
||||
#define SX126X_RESET (0 + 12)
|
||||
#define SX126X_DIO2_AS_RF_SWITCH true
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
#define PIN_SPI_MISO (32 + 9)
|
||||
#define PIN_SPI_MOSI (0 + 5)
|
||||
#define PIN_SPI_SCK (0 + 4)
|
||||
|
||||
#define LORA_CS SX126X_CS
|
||||
#define P_LORA_DIO_1 SX126X_DIO1
|
||||
#define P_LORA_NSS SX126X_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
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// EInk
|
||||
|
||||
#define PIN_DISPLAY_CS (24)
|
||||
#define PIN_DISPLAY_BUSY (32 + 6)
|
||||
#define PIN_DISPLAY_DC (31)
|
||||
#define PIN_DISPLAY_RST (32 + 4)
|
||||
|
||||
#define PIN_SPI1_MISO (-1)
|
||||
#define PIN_SPI1_MOSI (20)
|
||||
#define PIN_SPI1_SCK (22)
|
||||
|
||||
|
||||
// GxEPD2 needs that for a panel that is not even used !
|
||||
extern const int MISO;
|
||||
extern const int MOSI;
|
||||
extern const int SCK;
|
||||
|
||||
|
||||
#undef HAS_GPS
|
||||
#define HAS_GPS 0
|
||||
#define HAS_RTC 0
|
||||
Reference in New Issue
Block a user