mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-02 03:22:38 +02:00
tdpro audio only - voice notes over lora - 5 seconds stage 1
This commit is contained in:
@@ -604,6 +604,13 @@ void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk
|
||||
void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
|
||||
// Detect VE3 voice envelope and notify voice handler
|
||||
if (_voiceEnvHandler && text && strncmp(text, "VE3:", 4) == 0) {
|
||||
MESH_DEBUG_PRINTLN("Voice: VE3 envelope from %s: %s", from.name, text);
|
||||
_voiceEnvHandler(from.name, text);
|
||||
}
|
||||
|
||||
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
||||
}
|
||||
|
||||
@@ -746,6 +753,31 @@ bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::uiSendRawToContact(uint32_t contact_idx, const uint8_t* data, uint8_t len) {
|
||||
ContactInfo contact;
|
||||
if (!getContactByIdx(contact_idx, contact)) return false;
|
||||
|
||||
ContactInfo* recipient = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
|
||||
if (!recipient) return false;
|
||||
|
||||
// Raw custom packets are direct-route only — cannot flood
|
||||
if (recipient->out_path_len == OUT_PATH_UNKNOWN) {
|
||||
MESH_DEBUG_PRINTLN("UI: Raw send to %s failed — no direct path", recipient->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
mesh::Packet* pkt = createRawData(data, len);
|
||||
if (!pkt) {
|
||||
MESH_DEBUG_PRINTLN("UI: Raw send to %s failed — packet pool empty", recipient->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
sendDirect(pkt, recipient->out_path, recipient->out_path_len);
|
||||
MESH_DEBUG_PRINTLN("UI: Raw sent %d bytes to %s (direct, path_len=0x%02X)",
|
||||
len, recipient->name, recipient->out_path_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::uiLoginToRepeater(uint32_t contact_idx, const char* password, uint32_t& est_timeout_ms) {
|
||||
ContactInfo contact;
|
||||
if (!getContactByIdx(contact_idx, contact)) {
|
||||
@@ -1095,6 +1127,32 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
||||
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log ALL incoming raw packets for diagnosis
|
||||
Serial.printf("onRawDataRecv: len=%d, magic=0x%02X, route=%s\n",
|
||||
packet->payload_len,
|
||||
packet->payload_len > 0 ? packet->payload[0] : 0,
|
||||
packet->isRouteDirect() ? "direct" : "flood");
|
||||
|
||||
// Voice-over-LoRa (dz0ny VE3 protocol): intercept voice packets and fetch requests
|
||||
// before forwarding to BLE companion. In standalone mode (no BLE), this is the
|
||||
// only way to handle them. In BLE mode, we still intercept so on-device voice works.
|
||||
if (packet->payload_len > 1 && _voiceHandler) {
|
||||
uint8_t magic = packet->payload[0];
|
||||
if (magic == 0x56 || magic == 0x72) { // Voice data (V) or fetch request (r)
|
||||
Serial.printf("onRawDataRecv: voice %s, payload_len=%d, first6=[%02X %02X %02X %02X %02X %02X]\n",
|
||||
magic == 0x56 ? "PKT" : "FETCH", packet->payload_len,
|
||||
packet->payload[0],
|
||||
packet->payload_len > 1 ? packet->payload[1] : 0,
|
||||
packet->payload_len > 2 ? packet->payload[2] : 0,
|
||||
packet->payload_len > 3 ? packet->payload[3] : 0,
|
||||
packet->payload_len > 4 ? packet->payload[4] : 0,
|
||||
packet->payload_len > 5 ? packet->payload[5] : 0);
|
||||
_voiceHandler(magic, packet->payload, packet->payload_len);
|
||||
// Don't return — still forward to BLE companion if connected
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
out_frame[i++] = PUSH_CODE_RAW_DATA;
|
||||
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
|
||||
@@ -3073,14 +3131,17 @@ void MyMesh::loop() {
|
||||
|
||||
// is there are pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
if (!_store->isSaveInProgress()) {
|
||||
if (_deferSaves) {
|
||||
// Voice session receiving — push save forward to avoid SPI contention
|
||||
dirty_contacts_expiry = futureMillis(2000);
|
||||
} else if (!_store->isSaveInProgress()) {
|
||||
_store->beginSaveContacts(this);
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
|
||||
// Drive chunked contact save — write a batch each loop iteration
|
||||
if (_store->isSaveInProgress()) {
|
||||
if (_store->isSaveInProgress() && !_deferSaves) {
|
||||
if (!_store->saveContactsChunk(20)) { // 20 contacts per chunk (~3KB, ~30ms)
|
||||
_store->finishSaveContacts(); // Done or error — verify and commit
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 10
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "28 March 2026"
|
||||
#define FIRMWARE_BUILD_DATE "29 March 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v1.5"
|
||||
#define FIRMWARE_VERSION "Meck v1.6"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -133,6 +133,25 @@ public:
|
||||
// Send a direct message from the UI (no BLE dependency)
|
||||
bool uiSendDirectMessage(uint32_t contact_idx, const char* text);
|
||||
|
||||
// Send raw binary data to a contact (PAYLOAD_TYPE_RAW_CUSTOM, direct route only)
|
||||
// Used for dz0ny VE3 voice protocol: voice packets (0x56) and fetch requests (0x72)
|
||||
bool uiSendRawToContact(uint32_t contact_idx, const uint8_t* data, uint8_t len);
|
||||
|
||||
// Voice-over-LoRa: callback for incoming raw voice packets (dz0ny VE3 protocol)
|
||||
// magic 0x56 = voice data packet, 0x72 = fetch request
|
||||
typedef void (*VoiceRawHandler)(uint8_t magic, const uint8_t* payload, uint8_t len);
|
||||
void setVoiceHandler(VoiceRawHandler h) { _voiceHandler = h; }
|
||||
|
||||
// Voice-over-LoRa: callback for incoming VE3 envelope in a DM
|
||||
// Called with sender name and the VE3 text (e.g. "VE3:a:1:3:2")
|
||||
typedef void (*VoiceEnvelopeHandler)(const char* senderName, const char* ve3Text);
|
||||
void setVoiceEnvelopeHandler(VoiceEnvelopeHandler h) { _voiceEnvHandler = h; }
|
||||
|
||||
// Defer contact saves while voice packets are being received
|
||||
// (SD writes block SPI bus shared with LoRa radio)
|
||||
void setDeferSaves(bool defer) { _deferSaves = defer; }
|
||||
bool isDeferSaves() const { return _deferSaves; }
|
||||
|
||||
// Repeater admin - UI-initiated operations
|
||||
bool uiLoginToRepeater(uint32_t contact_idx, const char* password, uint32_t& est_timeout_ms);
|
||||
bool uiSendCliCommand(uint32_t contact_idx, const char* command);
|
||||
@@ -230,6 +249,9 @@ private:
|
||||
|
||||
DataStore* _store;
|
||||
NodePrefs _prefs;
|
||||
VoiceRawHandler _voiceHandler = nullptr;
|
||||
VoiceEnvelopeHandler _voiceEnvHandler = nullptr;
|
||||
bool _deferSaves = false;
|
||||
uint32_t pending_login;
|
||||
uint32_t pending_status;
|
||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||
|
||||
@@ -74,7 +74,11 @@
|
||||
#include "Audio.h"
|
||||
Audio* audio = nullptr;
|
||||
#endif
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
#include "VoiceMessageScreen.h"
|
||||
#endif
|
||||
static bool audiobookMode = false;
|
||||
static bool voiceMode = false;
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
#include "ModemManager.h"
|
||||
@@ -651,6 +655,78 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
|
||||
/* END GLOBAL OBJECTS */
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Voice-over-LoRa: incoming raw packet handler (dz0ny VE3 protocol)
|
||||
// Registered with the_mesh.setVoiceHandler() when voice screen is created.
|
||||
// Called from onRawDataRecv for magic 0x56 (voice data) and 0x72 (fetch req).
|
||||
// ---------------------------------------------------------------------------
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
// Helper: ensure voice screen exists (lazy-init for incoming voice)
|
||||
static VoiceMessageScreen* ensureVoiceScreen() {
|
||||
VoiceMessageScreen* voiceScr = (VoiceMessageScreen*)ui_task.getVoiceScreen();
|
||||
if (!voiceScr) {
|
||||
Serial.println("Voice: Auto-creating voice screen for incoming message");
|
||||
if (!audio) audio = new Audio();
|
||||
voiceScr = new VoiceMessageScreen(&ui_task, audio);
|
||||
voiceScr->setSDReady(sdCardReady);
|
||||
ui_task.setVoiceScreen(voiceScr);
|
||||
}
|
||||
return voiceScr;
|
||||
}
|
||||
|
||||
static void voiceRawCallback(uint8_t magic, const uint8_t* payload, uint8_t len) {
|
||||
VoiceMessageScreen* voiceScr = ensureVoiceScreen();
|
||||
|
||||
if (magic == 0x72 && len >= 6) {
|
||||
// Fetch request: [0x72][sessionId:4B][flags:1B][requesterKey6:6B][missingCount:1B][indices...]
|
||||
uint32_t sessionId;
|
||||
memcpy(&sessionId, &payload[1], 4);
|
||||
Serial.printf("Voice: Fetch request for session 0x%08X\n", sessionId);
|
||||
|
||||
if (voiceScr->getOutSessionId() == sessionId && voiceScr->hasValidOutSession()) {
|
||||
uint8_t pktBuf[184];
|
||||
int totalPkts = voiceScr->getOutSessionPacketCount();
|
||||
Serial.printf("Voice: Serving %d packets for session 0x%08X\n", totalPkts, sessionId);
|
||||
|
||||
// Requester's 6-byte key prefix is at payload[6..11].
|
||||
// Look them up to get their path for sendDirect.
|
||||
if (len >= 12) {
|
||||
ContactInfo* requester = the_mesh.lookupContactByPubKey(&payload[6], 6);
|
||||
if (requester && requester->out_path_len != OUT_PATH_UNKNOWN) {
|
||||
for (int p = 0; p < totalPkts; p++) {
|
||||
int pktLen = voiceScr->buildVoicePacket(pktBuf, sizeof(pktBuf), sessionId, p);
|
||||
if (pktLen > 0) {
|
||||
mesh::Packet* raw = the_mesh.createRawData(pktBuf, pktLen);
|
||||
if (raw) {
|
||||
the_mesh.sendDirect(raw, requester->out_path, requester->out_path_len, p * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
Serial.printf("Voice: Served %d packets to %s\n", totalPkts, requester->name);
|
||||
} else {
|
||||
Serial.println("Voice: Fetch requester not found or no direct path");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.printf("Voice: No cached session 0x%08X for fetch\n", sessionId);
|
||||
}
|
||||
} else if (magic == 0x56 && len > 6) {
|
||||
// Incoming voice data packet — feed to incoming session accumulator
|
||||
voiceScr->onVoicePacketReceived(payload, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Voice envelope callback — called from MyMesh::onMessageRecv when a VE3: DM arrives
|
||||
static void voiceEnvelopeCallback(const char* senderName, const char* ve3Text) {
|
||||
VoiceMessageScreen* voiceScr = ensureVoiceScreen();
|
||||
voiceScr->onVE3Received(senderName, ve3Text);
|
||||
// Defer SD contact saves while voice packets are arriving —
|
||||
// SD writes block the SPI bus shared with LoRa radio
|
||||
the_mesh.setDeferSaves(true);
|
||||
Serial.println("Voice: Deferring contact saves during voice receive");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Last Heard: add/remove contact for selected entry.
|
||||
// Called from both touch double-tap (mapTouchTap) and keyboard Enter handler.
|
||||
#ifdef DISPLAY_CLASS
|
||||
@@ -1800,6 +1876,14 @@ void setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Register voice-over-LoRa callbacks early so incoming VE3 envelopes and
|
||||
// raw voice packets are handled even before user opens the voice screen.
|
||||
// The callbacks null-check the voice screen pointer, so they're safe at boot.
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
the_mesh.setVoiceHandler(voiceRawCallback);
|
||||
the_mesh.setVoiceEnvelopeHandler(voiceEnvelopeCallback);
|
||||
#endif
|
||||
|
||||
Serial.printf("setup() complete — free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
|
||||
@@ -1991,6 +2075,159 @@ void loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Voice message: service mic DMA capture + playback audio decode
|
||||
#if defined(LilyGo_TDeck_Pro) && defined(MECK_AUDIO_VARIANT)
|
||||
{
|
||||
VoiceMessageScreen* voiceScr = (VoiceMessageScreen*)ui_task.getVoiceScreen();
|
||||
if (voiceScr) {
|
||||
voiceScr->voiceTick();
|
||||
|
||||
// Sync shared audio pointer — playFile() may have recreated Audio*
|
||||
Audio* voiceAudio = voiceScr->getAudio();
|
||||
if (voiceAudio != audio) {
|
||||
Serial.println("Voice: Syncing shared Audio* after recreation");
|
||||
audio = voiceAudio;
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
|
||||
if (alarmScr) alarmScr->setAudio(audio);
|
||||
}
|
||||
|
||||
// Service audio decode for voice playback (shared Audio* object)
|
||||
if (voiceScr->isAudioActive()) {
|
||||
if (audio) audio->loop();
|
||||
cpuPower.setBoost();
|
||||
}
|
||||
|
||||
// Detect end-of-playback and refresh UI
|
||||
voiceScr->checkPlaybackFinished();
|
||||
if (voiceScr->consumePlaybackFinished()) {
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
|
||||
// --- Contact picker: load contacts when mode transitions to CONTACT_PICK ---
|
||||
static bool pickContactsLoaded = false;
|
||||
if (voiceScr->getMode() == VoiceMessageScreen::CONTACT_PICK) {
|
||||
if (!pickContactsLoaded) {
|
||||
// Build list of chat contacts with direct paths
|
||||
VoiceMessageScreen::PickContact pickBuf[40];
|
||||
int pickCount = 0;
|
||||
ContactInfo ci;
|
||||
for (int idx = 0; idx < the_mesh.getNumContacts() && pickCount < 40; idx++) {
|
||||
if (!the_mesh.getContactByIdx(idx, ci)) continue;
|
||||
if (ci.type != ADV_TYPE_CHAT) continue; // Only chat nodes
|
||||
if (ci.name[0] == '\0') continue;
|
||||
pickBuf[pickCount].meshIdx = idx;
|
||||
strncpy(pickBuf[pickCount].name, ci.name, 31);
|
||||
pickBuf[pickCount].name[31] = '\0';
|
||||
pickBuf[pickCount].type = ci.type;
|
||||
pickBuf[pickCount].hasDirect = (ci.out_path_len != OUT_PATH_UNKNOWN);
|
||||
pickCount++;
|
||||
}
|
||||
// Sort: direct-path contacts first, then alphabetical within each group
|
||||
std::sort(pickBuf, pickBuf + pickCount,
|
||||
[](const VoiceMessageScreen::PickContact& a, const VoiceMessageScreen::PickContact& b) {
|
||||
if (a.hasDirect != b.hasDirect) return a.hasDirect > b.hasDirect;
|
||||
return strcasecmp(a.name, b.name) < 0;
|
||||
});
|
||||
voiceScr->loadPickContacts(pickBuf, pickCount);
|
||||
pickContactsLoaded = true;
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
} else {
|
||||
pickContactsLoaded = false;
|
||||
}
|
||||
|
||||
// --- Detect confirmed send from contact picker ---
|
||||
// Queue all packets at once using sendDirect's built-in delay parameter.
|
||||
// This lets the Mesh Dispatcher handle timing internally, spacing
|
||||
// transmissions so the SPI bus (shared with SD card) isn't contended.
|
||||
int sendIdx = voiceScr->consumePendingSend();
|
||||
if (sendIdx >= 0 && voiceScr->hasCodec2Data()) {
|
||||
cpuPower.setBoost();
|
||||
uint32_t sessionId = (uint32_t)(millis() & 0xFFFFFFFF);
|
||||
|
||||
char envelope[64];
|
||||
voiceScr->formatEnvelope(envelope, sizeof(envelope), sessionId);
|
||||
|
||||
ui_task.showAlert("Sending voice...", 10000);
|
||||
bool dmOk = the_mesh.uiSendDirectMessage(sendIdx, envelope);
|
||||
Serial.printf("Voice: VE3 DM '%s' to idx %d: %s\n",
|
||||
envelope, sendIdx, dmOk ? "OK" : "FAIL");
|
||||
|
||||
if (dmOk) {
|
||||
// Look up recipient for direct sendDirect calls
|
||||
ContactInfo ci;
|
||||
the_mesh.getContactByIdx(sendIdx, ci);
|
||||
ContactInfo* recipient = the_mesh.lookupContactByPubKey(ci.id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
int totalPkts = voiceScr->getOutSessionPacketCount();
|
||||
int sentPkts = 0;
|
||||
|
||||
if (recipient && recipient->out_path_len != OUT_PATH_UNKNOWN) {
|
||||
for (int p = 0; p < totalPkts; p++) {
|
||||
uint8_t pktBuf[184];
|
||||
int pktLen = voiceScr->buildVoicePacket(pktBuf, sizeof(pktBuf), sessionId, p);
|
||||
if (pktLen > 0) {
|
||||
mesh::Packet* raw = the_mesh.createRawData(pktBuf, pktLen);
|
||||
if (raw) {
|
||||
// Stagger packets: first at 3s (after VE3 + ACK + contact save),
|
||||
// each subsequent 3s apart. The Dispatcher queues them all now
|
||||
// and transmits at the specified delay offsets.
|
||||
uint32_t delayMs = 3000 + (uint32_t)p * 3000;
|
||||
the_mesh.sendDirect(raw, recipient->out_path, recipient->out_path_len, delayMs);
|
||||
sentPkts++;
|
||||
Serial.printf("Voice: Queued packet %d/%d (delay %dms)\n",
|
||||
p + 1, totalPkts, delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Serial.printf("Voice: Queued %d/%d voice packets to %s\n",
|
||||
sentPkts, totalPkts, recipient ? recipient->name : "?");
|
||||
voiceScr->onSendComplete(sentPkts == totalPkts);
|
||||
ui_task.showAlert(sentPkts == totalPkts ? "Voice sent!" : "Send partial", 2000);
|
||||
} else {
|
||||
voiceScr->onSendComplete(false);
|
||||
ui_task.showAlert("Send failed!", 1500);
|
||||
}
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
|
||||
// --- Auto-play incoming voice session when all packets received ---
|
||||
if (voiceScr->isIncomingReady()) {
|
||||
the_mesh.setDeferSaves(false); // Resume contact saves
|
||||
Serial.println("Voice: Incoming session complete — auto-playing");
|
||||
cpuPower.setBoost();
|
||||
if (voiceScr->playIncoming()) {
|
||||
ui_task.showAlert("Voice msg received!", 2000);
|
||||
ui_task.gotoVoiceScreen();
|
||||
} else {
|
||||
ui_task.showAlert("Voice decode failed", 1500);
|
||||
}
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
|
||||
// Safety timeout: if saves are deferred for more than 15s, resume them
|
||||
// (in case voice packets never arrive or session is abandoned)
|
||||
static unsigned long deferStarted = 0;
|
||||
if (the_mesh.isDeferSaves()) {
|
||||
if (deferStarted == 0) deferStarted = millis();
|
||||
if (millis() - deferStarted > 15000) {
|
||||
the_mesh.setDeferSaves(false);
|
||||
deferStarted = 0;
|
||||
Serial.println("Voice: Save defer timeout — resuming saves");
|
||||
}
|
||||
} else {
|
||||
deferStarted = 0;
|
||||
}
|
||||
|
||||
// During recording: keep CPU fast for DMA reads
|
||||
if (voiceScr->isRecording()) {
|
||||
cpuPower.setBoost();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// SMS: poll for incoming messages from modem
|
||||
#ifdef HAS_4G_MODEM
|
||||
{
|
||||
@@ -2168,6 +2405,9 @@ void loop() {
|
||||
readerMode = ui_task.isOnTextReader();
|
||||
notesMode = ui_task.isOnNotesScreen();
|
||||
audiobookMode = ui_task.isOnAudiobookPlayer();
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
voiceMode = ui_task.isOnVoiceScreen();
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
smsMode = ui_task.isOnSMSScreen();
|
||||
#endif
|
||||
@@ -2648,6 +2888,10 @@ void handleKeyboardInput() {
|
||||
}
|
||||
|
||||
// In compose mode - handle text input
|
||||
|
||||
// Ignore mic key press/release while composing text
|
||||
if (key == KB_KEY_MIC || key == KB_KEY_MIC_RELEASE) return;
|
||||
|
||||
if (key == '\r') {
|
||||
// Enter - send the message
|
||||
Serial.println("Compose: Enter pressed, sending...");
|
||||
@@ -2838,6 +3082,38 @@ void handleKeyboardInput() {
|
||||
}
|
||||
#endif // !HAS_4G_MODEM
|
||||
|
||||
// *** VOICE MESSAGE MODE ***
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
if (voiceMode) {
|
||||
VoiceMessageScreen* voiceScr = (VoiceMessageScreen*)ui_task.getVoiceScreen();
|
||||
if (!voiceScr) { voiceMode = false; }
|
||||
else {
|
||||
// Mic key press starts recording (PTT)
|
||||
if (key == KB_KEY_MIC) {
|
||||
voiceScr->onMicPress();
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
// Mic key release stops recording
|
||||
if (key == KB_KEY_MIC_RELEASE) {
|
||||
voiceScr->onMicRelease();
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
// Q from message list exits voice screen
|
||||
if (key == 'q' && voiceScr->getMode() == VoiceMessageScreen::MESSAGE_LIST) {
|
||||
Serial.println("Exiting voice message screen");
|
||||
ui_task.gotoHomeScreen();
|
||||
return;
|
||||
}
|
||||
// All other keys pass through to voice screen
|
||||
voiceScr->handleInput(key);
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif // MECK_AUDIO_VARIANT
|
||||
|
||||
// *** TEXT READER MODE ***
|
||||
if (readerMode) {
|
||||
TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen();
|
||||
@@ -3195,6 +3471,35 @@ void handleKeyboardInput() {
|
||||
#endif
|
||||
|
||||
// Normal mode - not composing
|
||||
|
||||
// Mic key release outside voice screen — ignore (PTT only matters on voice screen)
|
||||
if (key == KB_KEY_MIC_RELEASE) return;
|
||||
|
||||
// Mic key press from any non-modal screen — open voice message screen
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
if (key == KB_KEY_MIC) {
|
||||
Serial.println("Opening voice message screen (mic key)");
|
||||
if (!ui_task.getVoiceScreen()) {
|
||||
Serial.printf("Voice: lazy init - free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
if (!audio) audio = new Audio();
|
||||
VoiceMessageScreen* voiceScr = new VoiceMessageScreen(&ui_task, audio);
|
||||
voiceScr->setSDReady(sdCardReady);
|
||||
ui_task.setVoiceScreen(voiceScr);
|
||||
Serial.printf("Voice: init complete - free heap: %d\n", ESP.getFreeHeap());
|
||||
} else {
|
||||
// Ensure Audio* is shared (may have been created by audiobook/alarm)
|
||||
VoiceMessageScreen* voiceScr = (VoiceMessageScreen*)ui_task.getVoiceScreen();
|
||||
if (!audio) audio = new Audio();
|
||||
voiceScr->setAudio(audio);
|
||||
}
|
||||
ui_task.gotoVoiceScreen();
|
||||
// Don't start recording here — user tapped mic to navigate.
|
||||
// Recording starts on mic press when already on voice screen.
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (key) {
|
||||
case 'c':
|
||||
// Open contacts list
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "SettingsScreen.h"
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
#include "AudiobookPlayerScreen.h"
|
||||
#include "VoiceMessageScreen.h"
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
#include "SMSScreen.h"
|
||||
@@ -1298,6 +1299,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
audiobook_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
alarm_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
|
||||
voice_screen = nullptr; // Created and assigned from main.cpp on first mic key press
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
sms_screen = new SMSScreen(this, node_prefs);
|
||||
@@ -2654,6 +2656,20 @@ void UITask::gotoAlarmScreen() {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoVoiceScreen() {
|
||||
if (voice_screen == nullptr) return;
|
||||
VoiceMessageScreen* voiceScr = (VoiceMessageScreen*)voice_screen;
|
||||
if (_display != NULL) {
|
||||
voiceScr->enter(*_display);
|
||||
}
|
||||
setCurrScreen(voice_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
|
||||
@@ -88,6 +88,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* audiobook_screen; // Audiobook player screen (null if not available)
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
UIScreen* alarm_screen; // Alarm clock screen (audio variant only)
|
||||
UIScreen* voice_screen; // Voice message screen (audio variant only)
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
UIScreen* sms_screen; // SMS messaging screen (4G variant only)
|
||||
@@ -188,6 +189,7 @@ public:
|
||||
void gotoAudiobookPlayer(); // Navigate to audiobook player
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
void gotoAlarmScreen(); // Navigate to alarm clock
|
||||
void gotoVoiceScreen(); // Navigate to voice message recorder
|
||||
#endif
|
||||
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
|
||||
void gotoRepeaterAdminDirect(int contactIdx); // Auto-login admin (L key from conversation)
|
||||
@@ -240,6 +242,7 @@ public:
|
||||
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
bool isOnAlarmScreen() const { return curr == alarm_screen; }
|
||||
bool isOnVoiceScreen() const { return curr == voice_screen; }
|
||||
#endif
|
||||
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
|
||||
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
|
||||
@@ -312,6 +315,8 @@ public:
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
UIScreen* getAlarmScreen() const { return alarm_screen; }
|
||||
void setAlarmScreen(UIScreen* s) { alarm_screen = s; }
|
||||
UIScreen* getVoiceScreen() const { return voice_screen; }
|
||||
void setVoiceScreen(UIScreen* s) { voice_screen = s; }
|
||||
#endif
|
||||
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
|
||||
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
|
||||
|
||||
1531
examples/companion_radio/ui-new/Voicemessagescreen.h
Normal file
1531
examples/companion_radio/ui-new/Voicemessagescreen.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,9 @@
|
||||
#define KB_KEY_BACKSPACE '\b'
|
||||
#define KB_KEY_ENTER '\r'
|
||||
#define KB_KEY_SPACE ' '
|
||||
#define KB_KEY_EMOJI 0x01 // Non-printable code for $ key (emoji picker)
|
||||
#define KB_KEY_EMOJI 0x01 // Non-printable code for $ key (emoji picker)
|
||||
#define KB_KEY_MIC 0x02 // Mic key press (PTT start / voice screen open)
|
||||
#define KB_KEY_MIC_RELEASE 0x03 // Mic key release (PTT stop)
|
||||
|
||||
class TCA8418Keyboard {
|
||||
private:
|
||||
@@ -34,6 +36,7 @@ private:
|
||||
bool _shiftUsedWhileHeld; // Was shift consumed by any key while held
|
||||
bool _altActive; // Sticky alt (one-shot)
|
||||
bool _symActive; // Sticky sym (one-shot)
|
||||
bool _micHeld; // Mic key physically held down (for PTT release detection)
|
||||
unsigned long _lastShiftTime; // For Shift+key combos
|
||||
|
||||
uint8_t readReg(uint8_t reg) {
|
||||
@@ -151,7 +154,7 @@ private:
|
||||
public:
|
||||
TCA8418Keyboard(uint8_t addr = 0x34, TwoWire* wire = &Wire)
|
||||
: _addr(addr), _wire(wire), _initialized(false),
|
||||
_shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _lastShiftTime(0) {}
|
||||
_shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _micHeld(false), _lastShiftTime(0) {}
|
||||
|
||||
bool begin() {
|
||||
// Check if device responds
|
||||
@@ -242,6 +245,16 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Track mic key release — return KB_KEY_MIC_RELEASE for PTT stop
|
||||
if (!pressed && keyCode == 34) {
|
||||
if (_micHeld) {
|
||||
_micHeld = false;
|
||||
Serial.println("KB: Mic released -> KB_KEY_MIC_RELEASE");
|
||||
return KB_KEY_MIC_RELEASE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only act on key press, not release
|
||||
if (!pressed || keyCode == 0) {
|
||||
return 0;
|
||||
@@ -279,12 +292,17 @@ public:
|
||||
return KB_KEY_EMOJI;
|
||||
}
|
||||
|
||||
// Handle Mic key - always produces '0' (silk-screened on key)
|
||||
// Sym+Mic also produces '0' (consumes sym so it doesn't leak)
|
||||
// Handle Mic key — bare press returns KB_KEY_MIC for PTT / voice screen
|
||||
// Sym+Mic produces '0' (silk-screened on key) for text input
|
||||
if (keyCode == 34) {
|
||||
_symActive = false;
|
||||
Serial.println("KB: Mic -> '0'");
|
||||
return '0';
|
||||
if (_symActive) {
|
||||
_symActive = false;
|
||||
Serial.println("KB: Sym+Mic -> '0'");
|
||||
return '0';
|
||||
}
|
||||
_micHeld = true;
|
||||
Serial.println("KB: Mic -> KB_KEY_MIC");
|
||||
return KB_KEY_MIC;
|
||||
}
|
||||
|
||||
// Get the character
|
||||
@@ -338,6 +356,7 @@ public:
|
||||
}
|
||||
|
||||
bool isReady() const { return _initialized; }
|
||||
bool isMicHeld() const { return _micHeld; }
|
||||
|
||||
// Check if shift was pressed within the last N milliseconds
|
||||
bool wasShiftRecentlyPressed(unsigned long withinMs = 500) const {
|
||||
|
||||
@@ -129,6 +129,7 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
https://github.com/sh123/esp32_codec2_arduino.git
|
||||
|
||||
; Audio + WiFi companion (audio-player hardware with WiFi app bridging)
|
||||
; No BLE — WiFi companion uses SerialWifiInterface (TCP socket on port 5000).
|
||||
@@ -151,7 +152,7 @@ build_flags =
|
||||
-D MECK_AUDIO_VARIANT
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.WiFi"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -163,6 +164,7 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
https://github.com/sh123/esp32_codec2_arduino.git
|
||||
|
||||
; Audio standalone (audio-player hardware, no BLE/WiFi — maximum battery life)
|
||||
; No MECK_WEB_READER: WiFi power draw conflicts with zero-radio-power design.
|
||||
@@ -189,6 +191,7 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
https://github.com/sh123/esp32_codec2_arduino.git
|
||||
|
||||
; 4G + BLE companion (4G modem hardware, no audio — GPIO conflict with PCM5102A)
|
||||
; MAX_CONTACTS=500 is near BLE protocol ceiling (MAX_CONTACTS/2 sent as uint8_t, max 510)
|
||||
@@ -204,7 +207,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.4G"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.4G"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -235,7 +238,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.4G.WiFi"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.4G.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -262,7 +265,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.4G.SA"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.6.4G.SA"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
|
||||
Reference in New Issue
Block a user