Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2144,3 +2144,40 @@ bool MyMesh::advert() {
return false;
}
}

// To check if there is pending work (for power saving)
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true;
#endif
return _mgr->getOutboundTotal() > 0;
}

#ifdef MORSE_COMPOSE_ENABLED
void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* text) {
int i = 0;
if (app_target_ver >= 3) {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3;
out_frame[i++] = 0; // SNR = 0 (local)
out_frame[i++] = 0; // reserved1
out_frame[i++] = 0; // reserved2
} else {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV;
}
out_frame[i++] = channel_idx;
out_frame[i++] = 0; // path_len = 0 (local, zero hops)
out_frame[i++] = TXT_TYPE_PLAIN;
memcpy(&out_frame[i], &timestamp, 4); i += 4;
int tlen = strlen(text);
if (i + tlen > MAX_FRAME_SIZE) tlen = MAX_FRAME_SIZE - i;
memcpy(&out_frame[i], text, tlen);
i += tlen;
addToOfflineQueue(out_frame, i);

if (_serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING;
_serial->writeFrame(frame, 1);
}
}
#endif
13 changes: 11 additions & 2 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#define FIRMWARE_VER_CODE 10

#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
#define FIRMWARE_BUILD_DATE "17 Apr 2026"
#endif

#ifndef FIRMWARE_VERSION
Expand Down Expand Up @@ -165,6 +165,15 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }

#ifdef MORSE_COMPOSE_ENABLED
// Queue a locally-originated channel message for BLE companion app sync.
// Called from UITask after MorseScreen sends via sendGroupMessage().
void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* text);
#endif

// To check if there is pending work (for power saving)
bool hasPendingWork() const;

#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
Expand Down Expand Up @@ -248,4 +257,4 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};

extern MyMesh the_mesh;
extern MyMesh the_mesh;
23 changes: 22 additions & 1 deletion examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ void setup() {
#ifdef DISPLAY_CLASS
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
#endif

// [DEBUG] Uncomment to check free heap / offline queue sizing:
// Serial.println("[HEAP] === After setup ===");
// dbgMemInfo();
// Serial.printf("[HEAP] OFFLINE_QUEUE_SIZE=%d, frame size=%d bytes, queue total=%d bytes\n",
// OFFLINE_QUEUE_SIZE, (int)(1 + MAX_FRAME_SIZE),
// OFFLINE_QUEUE_SIZE * (int)(1 + MAX_FRAME_SIZE));
}

void loop() {
Expand All @@ -229,4 +236,18 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
}

if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nRF52 ignores seconds param, sleeps until next interrupt
#endif
}

// [DEBUG] Uncomment for periodic heap monitoring:
// static unsigned long next_heap_print = 30000;
// if (millis() > next_heap_print) {
// Serial.println("[HEAP] === Periodic ===");
// dbgMemInfo();
// next_heap_print = millis() + 60000;
// }
}
109 changes: 108 additions & 1 deletion examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@

#include "icons.h"

#ifdef MORSE_COMPOSE_ENABLED
#include "MorseScreen.h"
#endif

class SplashScreen : public UIScreen {
UITask* _task;
unsigned long dismiss_after;
Expand Down Expand Up @@ -560,6 +564,18 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no

_node_prefs = node_prefs;

#if ENV_INCLUDE_GPS == 1
// Apply GPS preferences from stored prefs
if (_sensors != NULL && _node_prefs != NULL) {
_sensors->setSettingValue("gps", _node_prefs->gps_enabled ? "1" : "0");
if (_node_prefs->gps_interval > 0) {
char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null)
sprintf(interval_str, "%u", _node_prefs->gps_interval);
_sensors->setSettingValue("gps_interval", interval_str);
}
}
#endif

if (_display != NULL) {
_display->turnOn();
}
Expand All @@ -578,7 +594,13 @@ 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
#ifdef MORSE_COMPOSE_ENABLED
morse_screen = new MorseScreen(&rtc_clock);
morse_channel_picker = new MorseChannelPicker();
#endif
setCurrScreen(splash);
}

Expand Down Expand Up @@ -627,8 +649,22 @@ void UITask::msgRead(int msgcount) {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;

#ifndef HELTEC_MESH_POCKET
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
#ifdef MORSE_COMPOSE_ENABLED
// Don't switch away from MorseScreen — incoming messages are shown in its
// inbox instead. Switching mid-hold would break the exit gesture.
if (curr != morse_screen)
#endif
setCurrScreen(msg_preview);
#endif

#ifdef MORSE_COMPOSE_ENABLED
// Feed all incoming messages to MorseScreen inbox for display
if (morse_screen) {
((MorseScreen*)morse_screen)->notifyPublicMsg(from_name, text);
}
#endif

if (_display != NULL) {
if (!_display->isOn() && !hasConnection()) {
Expand Down Expand Up @@ -728,6 +764,12 @@ void UITask::loop() {
c = handleTripleClick(KEY_SELECT);
}
#elif defined(PIN_USER_BTN)
#ifdef MORSE_COMPOSE_ENABLED
// MorseScreen handles button timing directly via isPressed() in its poll().
// Skip MomentaryButton event processing to avoid dot/dash presses being
// misinterpreted as clicks/double-clicks/triple-clicks.
if (curr != morse_screen) {
#endif
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
Expand All @@ -738,6 +780,9 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#ifdef MORSE_COMPOSE_ENABLED
}
#endif
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
Expand Down Expand Up @@ -780,6 +825,49 @@ void UITask::loop() {

if (curr) curr->poll();

#ifdef MORSE_COMPOSE_ENABLED
// Channel picker → MorseScreen transition
if (curr == morse_channel_picker) {
MorseChannelPicker* picker = (MorseChannelPicker*)morse_channel_picker;
if (picker->isConfirmed()) {
uint8_t ch_idx = picker->getSelectedChannelIdx();
const char* ch_name = picker->getSelectedChannelName();
((MorseScreen*)morse_screen)->activate(ch_idx, ch_name);
setCurrScreen(morse_screen);
picker->acknowledgeConfirm();
}
if (picker->wantsExit()) {
picker->acknowledgeExit();
gotoHomeScreen();
}
}

// MorseScreen send/exit handling
if (curr == morse_screen) {
MorseScreen* ms = (MorseScreen*)morse_screen;
if (ms->wantsExit()) {
ms->acknowledgeExit();
gotoHomeScreen();
}
const char* sendText = nullptr;
if (ms->consumeSendRequest(&sendText) && sendText) {
uint8_t ch_idx = ms->getChannelIdx();
ChannelDetails ch;
if (the_mesh.getChannel(ch_idx, ch)) {
uint32_t ts = rtc_clock.getCurrentTime();
the_mesh.sendGroupMessage(ts, ch.channel,
the_mesh.getNodeName(), sendText, strlen(sendText));
char fullMsg[160];
snprintf(fullMsg, sizeof(fullMsg), "%s: %s",
the_mesh.getNodeName(), sendText);
the_mesh.queueSentChannelMessage(ch_idx, ts, fullMsg);
showAlert("Sent!", 800);
}
ms->clearOutBuf();
}
}
#endif

if (_display != NULL && _display->isOn()) {
if (millis() >= _next_refresh && curr) {
_display->startFrame();
Expand Down Expand Up @@ -859,6 +947,25 @@ char UITask::handleLongPress(char c) {
char UITask::handleDoubleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: double click triggered");
checkDisplayOn(c);
#ifdef MORSE_COMPOSE_ENABLED
if (curr == home) {
// Populate channel picker with available channels
MorseChannelPicker* picker = (MorseChannelPicker*)morse_channel_picker;
picker->activate();
ChannelDetails ch;
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
if (the_mesh.getChannel(i, ch) && ch.name[0] != 0) {
picker->addChannel(i, ch.name);
}
}
setCurrScreen(morse_channel_picker);
// [DEBUG] Uncomment to check heap at Morse entry:
// Serial.println("[HEAP] === Morse entry ===");
// dbgMemInfo();
c = 0;
return c;
}
#endif
return c;
}

Expand Down Expand Up @@ -920,4 +1027,4 @@ void UITask::toggleBuzzer() {
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
_next_refresh = 0; // trigger refresh
#endif
}
}
8 changes: 7 additions & 1 deletion examples/companion_radio/ui-new/UITask.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ class UITask : public AbstractUITask {

UIScreen* splash;
UIScreen* home;
#ifndef HELTEC_MESH_POCKET
UIScreen* msg_preview;
#endif
#ifdef MORSE_COMPOSE_ENABLED
UIScreen* morse_screen;
UIScreen* morse_channel_picker;
#endif
UIScreen* curr;

void userLedHandler();
Expand Down Expand Up @@ -98,4 +104,4 @@ class UITask : public AbstractUITask {
void loop() override;

void shutdown(bool restart = false);
};
};
43 changes: 33 additions & 10 deletions variants/lilygo_techo_lite/TechoBoard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,47 @@
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); // give sx1262 some time to power up
delay(10);
}

uint16_t TechoBoard::getBattMilliVolts() {
int adcvalue = 0;

// Use LilyGo's exact ADC configuration
analogReference(AR_INTERNAL_3_0);
analogReadResolution(12);
delay(10);

// ADC range is 0..3000mV and resolution is 12-bit (0..4095)
adcvalue = analogRead(PIN_VBAT_READ);
// Convert the raw value to compensated mv, taking the resistor-
// divider into account (providing the actual LIPO voltage)
return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB);
// 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
#endif
Loading