mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-02 14:52:39 +02:00
phone calls! woo
This commit is contained in:
@@ -714,6 +714,58 @@ void loop() {
|
||||
|
||||
Serial.printf("[SMS] Received from %s: %.40s...\n", incoming.phone, incoming.body);
|
||||
}
|
||||
|
||||
// Poll for voice call events from modem
|
||||
CallEvent callEvt;
|
||||
while (modemManager.pollCallEvent(callEvt)) {
|
||||
SMSScreen* smsScr2 = (SMSScreen*)ui_task.getSMSScreen();
|
||||
if (smsScr2) {
|
||||
smsScr2->onCallEvent(callEvt);
|
||||
}
|
||||
|
||||
if (callEvt.type == CallEventType::INCOMING) {
|
||||
// Incoming call — auto-switch to SMS screen if not already there
|
||||
char alertBuf[48];
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(callEvt.phone, dispName, sizeof(dispName));
|
||||
snprintf(alertBuf, sizeof(alertBuf), "Call: %s", dispName);
|
||||
ui_task.showAlert(alertBuf, 3000);
|
||||
ui_task.notify(UIEventType::contactMessage);
|
||||
|
||||
if (!smsMode) {
|
||||
ui_task.gotoSMSScreen();
|
||||
}
|
||||
ui_task.forceRefresh();
|
||||
Serial.printf("[Call] Incoming from %s\n", callEvt.phone);
|
||||
} else if (callEvt.type == CallEventType::CONNECTED) {
|
||||
Serial.printf("[Call] Connected to %s\n", callEvt.phone);
|
||||
ui_task.forceRefresh();
|
||||
} else if (callEvt.type == CallEventType::ENDED) {
|
||||
Serial.printf("[Call] Ended (%lus) with %s\n",
|
||||
(unsigned long)callEvt.duration, callEvt.phone);
|
||||
ui_task.forceRefresh();
|
||||
} else if (callEvt.type == CallEventType::MISSED) {
|
||||
char alertBuf[48];
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(callEvt.phone, dispName, sizeof(dispName));
|
||||
snprintf(alertBuf, sizeof(alertBuf), "Missed: %s", dispName);
|
||||
ui_task.showAlert(alertBuf, 3000);
|
||||
Serial.printf("[Call] Missed from %s\n", callEvt.phone);
|
||||
ui_task.forceRefresh();
|
||||
} else if (callEvt.type == CallEventType::BUSY) {
|
||||
ui_task.showAlert("Line busy", 2000);
|
||||
Serial.printf("[Call] Busy: %s\n", callEvt.phone);
|
||||
ui_task.forceRefresh();
|
||||
} else if (callEvt.type == CallEventType::NO_ANSWER) {
|
||||
ui_task.showAlert("No answer", 2000);
|
||||
Serial.printf("[Call] No answer: %s\n", callEvt.phone);
|
||||
ui_task.forceRefresh();
|
||||
} else if (callEvt.type == CallEventType::DIAL_FAILED) {
|
||||
ui_task.showAlert("Call failed", 2000);
|
||||
Serial.printf("[Call] Dial failed: %s\n", callEvt.phone);
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef DISPLAY_CLASS
|
||||
@@ -1255,6 +1307,20 @@ void handleKeyboardInput() {
|
||||
if (smsMode) {
|
||||
SMSScreen* smsScr = (SMSScreen*)ui_task.getSMSScreen();
|
||||
if (smsScr) {
|
||||
// During active call views, route all keys directly to the screen
|
||||
// and force a refresh after each keypress (no debounce needed)
|
||||
if (smsScr->isInCallView()) {
|
||||
smsScr->handleInput(key);
|
||||
if (smsScr->isInCallView()) {
|
||||
// Still in a call view — refresh to update display
|
||||
ui_task.forceRefresh();
|
||||
} else {
|
||||
// Call ended, returned to a non-call view
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Q from inbox → go home; Q from inner views is handled by SMSScreen
|
||||
if ((key == 'q' || key == '\b') && smsScr->getSubView() == SMSScreen::INBOX) {
|
||||
Serial.println("Nav: SMS -> Home");
|
||||
@@ -1294,13 +1360,12 @@ void handleKeyboardInput() {
|
||||
bool urlEdit = wr ? wr->isUrlEditing() : false;
|
||||
bool passEdit = wr ? wr->isPasswordEntry() : false;
|
||||
bool formEdit = wr ? wr->isFormFilling() : false;
|
||||
bool ircEdit = wr ? wr->isIRCTextEntry() : false;
|
||||
if (wr && (urlEdit || passEdit || formEdit || ircEdit)) {
|
||||
if (wr && (urlEdit || passEdit || formEdit)) {
|
||||
webReaderTextEntry = true; // Suppress ui_task.loop() in main loop
|
||||
wr->handleInput(key); // Updates buffer instantly, no render
|
||||
|
||||
// Check if text entry ended (submitted, cancelled, etc.)
|
||||
if (!wr->isUrlEditing() && !wr->isPasswordEntry() && !wr->isFormFilling() && !wr->isIRCTextEntry()) {
|
||||
if (!wr->isUrlEditing() && !wr->isPasswordEntry() && !wr->isFormFilling()) {
|
||||
// Text entry ended
|
||||
webReaderTextEntry = false;
|
||||
webReaderNeedsRefresh = false;
|
||||
@@ -1319,29 +1384,12 @@ void handleKeyboardInput() {
|
||||
// Q from HOME mode exits the web reader entirely (like text reader)
|
||||
if ((key == 'q' || key == 'Q') && wr && wr->isHome() && !wr->isUrlEditing()) {
|
||||
Serial.println("Exiting web reader");
|
||||
wr->exitReader(); // Shut down WiFi, free buffers
|
||||
ui_task.gotoHomeScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// IRC modes: route keys directly through handleInput and render
|
||||
// immediately. Using webReaderNeedsRefresh doesn't work here because
|
||||
// that check only runs when webReaderTextEntry is true (else branch),
|
||||
// but in IRC non-compose mode webReaderTextEntry is false.
|
||||
if (wr && wr->isIRCMode()) {
|
||||
wr->handleInput(key);
|
||||
wr->directRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// Route keys through normal UITask for navigation/scrolling
|
||||
ui_task.injectKey(key);
|
||||
|
||||
// Check if web reader wants to switch to text reader (EPUB download)
|
||||
if (wr && wr->wantsTextReader()) {
|
||||
wr->clearTextReaderRequest();
|
||||
ui_task.gotoTextReader();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1407,18 +1455,11 @@ void handleKeyboardInput() {
|
||||
Serial.printf("WebReader: heap BEFORE BT release: free=%d, largest=%d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
|
||||
// 1) Gracefully disable BLE interface (stop advertising, disconnect, clear queues)
|
||||
// Must happen BEFORE btStop() while BLE objects are still valid
|
||||
#ifdef BLE_PIN_CODE
|
||||
serial_interface.disable();
|
||||
delay(50);
|
||||
#endif
|
||||
|
||||
// 2) Stop BLE controller (disable + deinit)
|
||||
// 1) Stop BLE controller (disable + deinit)
|
||||
btStop();
|
||||
delay(50);
|
||||
|
||||
// 3) Release the BT controller's reserved memory region back to heap
|
||||
// 2) Release the BT controller's reserved memory region back to heap
|
||||
esp_bt_controller_mem_release(ESP_BT_MODE_BTDM);
|
||||
delay(50);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ ModemManager modemManager;
|
||||
static char _atBuf[AT_BUF_SIZE];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// Public API - SMS (unchanged)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ModemManager::begin() {
|
||||
@@ -27,11 +27,16 @@ void ModemManager::begin() {
|
||||
_state = ModemState::OFF;
|
||||
_csq = 99;
|
||||
_operator[0] = '\0';
|
||||
_callPhone[0] = '\0';
|
||||
_callStartTime = 0;
|
||||
_urcPos = 0;
|
||||
|
||||
// Create FreeRTOS primitives
|
||||
_sendQueue = xQueueCreate(MODEM_SEND_QUEUE_SIZE, sizeof(SMSOutgoing));
|
||||
_recvQueue = xQueueCreate(MODEM_RECV_QUEUE_SIZE, sizeof(SMSIncoming));
|
||||
_uartMutex = xSemaphoreCreateMutex();
|
||||
_sendQueue = xQueueCreate(MODEM_SEND_QUEUE_SIZE, sizeof(SMSOutgoing));
|
||||
_recvQueue = xQueueCreate(MODEM_RECV_QUEUE_SIZE, sizeof(SMSIncoming));
|
||||
_callCmdQueue = xQueueCreate(MODEM_CALL_CMD_QUEUE_SIZE, sizeof(CallCommand));
|
||||
_callEvtQueue = xQueueCreate(MODEM_CALL_EVT_QUEUE_SIZE, sizeof(CallEvent));
|
||||
_uartMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Launch background task on Core 0
|
||||
xTaskCreatePinnedToCore(
|
||||
@@ -50,6 +55,15 @@ void ModemManager::shutdown() {
|
||||
|
||||
MESH_DEBUG_PRINTLN("[Modem] shutdown()");
|
||||
|
||||
// Hang up any active call first
|
||||
if (isCallActive()) {
|
||||
CallCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.cmd = CallCmd::HANGUP;
|
||||
xQueueSend(_callCmdQueue, &cmd, pdMS_TO_TICKS(500));
|
||||
vTaskDelay(pdMS_TO_TICKS(2000)); // Give time for AT+CHUP
|
||||
}
|
||||
|
||||
// Tell modem to power off gracefully
|
||||
if (xSemaphoreTake(_uartMutex, pdMS_TO_TICKS(2000))) {
|
||||
sendAT("AT+CPOF", "OK", 5000);
|
||||
@@ -81,6 +95,74 @@ bool ModemManager::recvSMS(SMSIncoming& out) {
|
||||
return xQueueReceive(_recvQueue, &out, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API - Voice Calls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ModemManager::dialCall(const char* phone) {
|
||||
if (!_callCmdQueue) return false;
|
||||
if (isCallActive()) return false; // Already in a call
|
||||
|
||||
CallCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.cmd = CallCmd::DIAL;
|
||||
strncpy(cmd.phone, phone, SMS_PHONE_LEN - 1);
|
||||
|
||||
return xQueueSend(_callCmdQueue, &cmd, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
bool ModemManager::answerCall() {
|
||||
if (!_callCmdQueue) return false;
|
||||
|
||||
CallCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.cmd = CallCmd::ANSWER;
|
||||
|
||||
return xQueueSend(_callCmdQueue, &cmd, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
bool ModemManager::hangupCall() {
|
||||
if (!_callCmdQueue) return false;
|
||||
|
||||
CallCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.cmd = CallCmd::HANGUP;
|
||||
|
||||
return xQueueSend(_callCmdQueue, &cmd, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
bool ModemManager::sendDTMF(char digit) {
|
||||
if (!_callCmdQueue) return false;
|
||||
if (_state != ModemState::IN_CALL) return false;
|
||||
|
||||
CallCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.cmd = CallCmd::DTMF;
|
||||
cmd.dtmf = digit;
|
||||
|
||||
return xQueueSend(_callCmdQueue, &cmd, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
bool ModemManager::setCallVolume(uint8_t level) {
|
||||
if (!_callCmdQueue) return false;
|
||||
|
||||
CallCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.cmd = CallCmd::SET_VOLUME;
|
||||
cmd.volume = level > 5 ? 5 : level;
|
||||
|
||||
return xQueueSend(_callCmdQueue, &cmd, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
bool ModemManager::pollCallEvent(CallEvent& out) {
|
||||
if (!_callEvtQueue) return false;
|
||||
return xQueueReceive(_callEvtQueue, &out, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int ModemManager::getSignalBars() const {
|
||||
if (_csq == 99 || _csq == 0) return 0;
|
||||
if (_csq <= 5) return 1;
|
||||
@@ -99,6 +181,9 @@ const char* ModemManager::stateToString(ModemState s) {
|
||||
case ModemState::READY: return "READY";
|
||||
case ModemState::ERROR: return "ERROR";
|
||||
case ModemState::SENDING_SMS: return "SENDING";
|
||||
case ModemState::DIALING: return "DIALING";
|
||||
case ModemState::RINGING_IN: return "INCOMING";
|
||||
case ModemState::IN_CALL: return "IN CALL";
|
||||
default: return "???";
|
||||
}
|
||||
}
|
||||
@@ -132,6 +217,282 @@ void ModemManager::saveEnabledConfig(bool enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// URC (Unsolicited Result Code) Handling
|
||||
// ---------------------------------------------------------------------------
|
||||
// The modem can send unsolicited messages at any time:
|
||||
// RING — incoming call ringing
|
||||
// +CLIP: "+1234...",145,... — caller ID (after AT+CLIP=1)
|
||||
// NO CARRIER — call ended by remote
|
||||
// BUSY — outgoing call busy
|
||||
// NO ANSWER — outgoing call no answer
|
||||
// +CMTI: "SM",<idx> — new SMS arrived
|
||||
//
|
||||
// drainURCs() accumulates bytes into a line buffer and calls
|
||||
// processURCLine() for each complete line.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ModemManager::drainURCs() {
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
|
||||
// Accumulate into line buffer
|
||||
if (c == '\n') {
|
||||
// End of line — process if non-empty
|
||||
if (_urcPos > 0) {
|
||||
// Trim trailing \r
|
||||
while (_urcPos > 0 && _urcBuf[_urcPos - 1] == '\r') _urcPos--;
|
||||
_urcBuf[_urcPos] = '\0';
|
||||
|
||||
if (_urcPos > 0) {
|
||||
processURCLine(_urcBuf);
|
||||
}
|
||||
}
|
||||
_urcPos = 0;
|
||||
} else if (c != '\r' || _urcPos > 0) {
|
||||
// Accumulate (skip leading \r)
|
||||
if (_urcPos < URC_BUF_SIZE - 1) {
|
||||
_urcBuf[_urcPos++] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModemManager::processURCLine(const char* line) {
|
||||
// --- RING: incoming call ---
|
||||
if (strcmp(line, "RING") == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: RING");
|
||||
if (_state != ModemState::RINGING_IN && _state != ModemState::IN_CALL) {
|
||||
_state = ModemState::RINGING_IN;
|
||||
// Phone number will be filled by +CLIP if available
|
||||
// Queue event with empty phone (updated by +CLIP)
|
||||
// Only queue on first RING; subsequent RINGs are repeats
|
||||
if (_callPhone[0] == '\0') {
|
||||
queueCallEvent(CallEventType::INCOMING, "");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- +CLIP: caller ID ---
|
||||
// +CLIP: "+61412345678",145,,,,0
|
||||
if (strncmp(line, "+CLIP:", 6) == 0) {
|
||||
char* q1 = strchr(line + 6, '"');
|
||||
if (q1) {
|
||||
q1++;
|
||||
char* q2 = strchr(q1, '"');
|
||||
if (q2) {
|
||||
int len = q2 - q1;
|
||||
if (len >= SMS_PHONE_LEN) len = SMS_PHONE_LEN - 1;
|
||||
memcpy(_callPhone, q1, len);
|
||||
_callPhone[len] = '\0';
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: CLIP phone=%s", _callPhone);
|
||||
|
||||
// Re-queue INCOMING event with the actual phone number
|
||||
// (replaces the empty-phone event from RING)
|
||||
if (_state == ModemState::RINGING_IN) {
|
||||
queueCallEvent(CallEventType::INCOMING, _callPhone);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- NO CARRIER: call ended ---
|
||||
if (strcmp(line, "NO CARRIER") == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: NO CARRIER");
|
||||
if (_state == ModemState::RINGING_IN) {
|
||||
// Incoming call ended before we answered — missed call
|
||||
queueCallEvent(CallEventType::MISSED, _callPhone);
|
||||
} else if (_state == ModemState::DIALING || _state == ModemState::IN_CALL) {
|
||||
uint32_t duration = 0;
|
||||
if (_state == ModemState::IN_CALL && _callStartTime > 0) {
|
||||
duration = (millis() - _callStartTime) / 1000;
|
||||
}
|
||||
queueCallEvent(CallEventType::ENDED, _callPhone, duration);
|
||||
}
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
_callStartTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- BUSY ---
|
||||
if (strcmp(line, "BUSY") == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: BUSY");
|
||||
if (_state == ModemState::DIALING) {
|
||||
queueCallEvent(CallEventType::BUSY, _callPhone);
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- NO ANSWER ---
|
||||
if (strcmp(line, "NO ANSWER") == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: NO ANSWER");
|
||||
if (_state == ModemState::DIALING) {
|
||||
queueCallEvent(CallEventType::NO_ANSWER, _callPhone);
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- +CMTI: new SMS indication ---
|
||||
// +CMTI: "SM",<index>
|
||||
// We don't need to act on this immediately since we poll for SMS,
|
||||
// but we can trigger an early poll
|
||||
if (strncmp(line, "+CMTI:", 6) == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: CMTI (new SMS)");
|
||||
// Next SMS poll will pick it up; we just log it
|
||||
return;
|
||||
}
|
||||
|
||||
// --- VOICE CALL: BEGIN — A76xx-specific: audio path established ---
|
||||
if (strncmp(line, "VOICE CALL: BEGIN", 17) == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: VOICE CALL: BEGIN");
|
||||
if (_state == ModemState::DIALING) {
|
||||
_state = ModemState::IN_CALL;
|
||||
_callStartTime = millis();
|
||||
queueCallEvent(CallEventType::CONNECTED, _callPhone);
|
||||
MESH_DEBUG_PRINTLN("[Modem] Call connected (VOICE CALL: BEGIN)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- VOICE CALL: END — A76xx-specific: audio path closed ---
|
||||
// Format: "VOICE CALL: END: <duration>"
|
||||
if (strncmp(line, "VOICE CALL: END", 15) == 0) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] URC: %s", line);
|
||||
// Parse duration if present: "VOICE CALL: END: 0:12"
|
||||
uint32_t duration = 0;
|
||||
const char* dp = strstr(line, "END:");
|
||||
if (dp) {
|
||||
dp += 4;
|
||||
while (*dp == ' ') dp++;
|
||||
int mins = 0, secs = 0;
|
||||
if (sscanf(dp, "%d:%d", &mins, &secs) == 2) {
|
||||
duration = mins * 60 + secs;
|
||||
}
|
||||
}
|
||||
if (_state == ModemState::RINGING_IN) {
|
||||
queueCallEvent(CallEventType::MISSED, _callPhone);
|
||||
} else if (_state == ModemState::IN_CALL || _state == ModemState::DIALING) {
|
||||
queueCallEvent(CallEventType::ENDED, _callPhone, duration);
|
||||
}
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
_callStartTime = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ModemManager::queueCallEvent(CallEventType type, const char* phone, uint32_t duration) {
|
||||
CallEvent evt;
|
||||
memset(&evt, 0, sizeof(evt));
|
||||
evt.type = type;
|
||||
evt.duration = duration;
|
||||
if (phone) {
|
||||
strncpy(evt.phone, phone, SMS_PHONE_LEN - 1);
|
||||
}
|
||||
xQueueSend(_callEvtQueue, &evt, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call control (executed on modem task)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ModemManager::doDialCall(const char* phone) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] doDialCall: %s", phone);
|
||||
|
||||
strncpy(_callPhone, phone, SMS_PHONE_LEN - 1);
|
||||
_callPhone[SMS_PHONE_LEN - 1] = '\0';
|
||||
_state = ModemState::DIALING;
|
||||
|
||||
// ATD<number>; — the semicolon makes it a voice call (not data)
|
||||
char cmd[32];
|
||||
snprintf(cmd, sizeof(cmd), "ATD%s;", phone);
|
||||
|
||||
if (!sendAT(cmd, "OK", 30000)) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] ATD failed");
|
||||
queueCallEvent(CallEventType::DIAL_FAILED, phone);
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
// ATD returned OK — call is being set up.
|
||||
// Connection/failure will come as URCs (NO CARRIER, BUSY, etc.)
|
||||
// or we detect active call via AT+CLCC polling.
|
||||
// For now, assume we're dialing and wait for URCs.
|
||||
MESH_DEBUG_PRINTLN("[Modem] ATD OK — dialing...");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ModemManager::doAnswerCall() {
|
||||
MESH_DEBUG_PRINTLN("[Modem] doAnswerCall");
|
||||
|
||||
if (sendAT("ATA", "OK", 10000)) {
|
||||
_state = ModemState::IN_CALL;
|
||||
_callStartTime = millis();
|
||||
queueCallEvent(CallEventType::CONNECTED, _callPhone);
|
||||
MESH_DEBUG_PRINTLN("[Modem] Call answered");
|
||||
return true;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("[Modem] ATA failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModemManager::doHangup() {
|
||||
MESH_DEBUG_PRINTLN("[Modem] doHangup (state=%d)", (int)_state);
|
||||
|
||||
uint32_t duration = 0;
|
||||
if (_state == ModemState::IN_CALL && _callStartTime > 0) {
|
||||
duration = (millis() - _callStartTime) / 1000;
|
||||
}
|
||||
|
||||
bool wasRinging = (_state == ModemState::RINGING_IN);
|
||||
|
||||
// AT+CHUP is the 3GPP standard hangup for A76xx family (per TinyGSM)
|
||||
if (sendAT("AT+CHUP", "OK", 5000)) {
|
||||
if (wasRinging) {
|
||||
queueCallEvent(CallEventType::MISSED, _callPhone);
|
||||
} else {
|
||||
queueCallEvent(CallEventType::ENDED, _callPhone, duration);
|
||||
}
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
_callStartTime = 0;
|
||||
MESH_DEBUG_PRINTLN("[Modem] Hangup OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("[Modem] AT+CHUP failed");
|
||||
// Force state back to READY even if hangup fails
|
||||
_state = ModemState::READY;
|
||||
_callPhone[0] = '\0';
|
||||
_callStartTime = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModemManager::doSendDTMF(char digit) {
|
||||
char cmd[16];
|
||||
snprintf(cmd, sizeof(cmd), "AT+VTS=%c", digit);
|
||||
bool ok = sendAT(cmd, "OK", 3000);
|
||||
MESH_DEBUG_PRINTLN("[Modem] DTMF '%c' %s", digit, ok ? "OK" : "FAIL");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ModemManager::doSetVolume(uint8_t level) {
|
||||
char cmd[16];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CLVL=%d", level);
|
||||
bool ok = sendAT(cmd, "OK", 2000);
|
||||
MESH_DEBUG_PRINTLN("[Modem] Volume %d %s", level, ok ? "OK" : "FAIL");
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FreeRTOS Task
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -188,6 +549,17 @@ restart:
|
||||
// Enable automatic time zone update from network (needed for AT+CCLK)
|
||||
sendAT("AT+CTZU=1", "OK");
|
||||
|
||||
// --- Voice call setup ---
|
||||
// Enable caller ID presentation (CLIP) so we get +CLIP URCs on incoming calls
|
||||
sendAT("AT+CLIP=1", "OK");
|
||||
|
||||
// Set audio output to loudspeaker mode (device speaker)
|
||||
// 1=earpiece, 3=loudspeaker — use loudspeaker for T-Deck Pro
|
||||
sendAT("AT+CSDVC=3", "OK", 1000);
|
||||
|
||||
// Set initial call volume (mid-level)
|
||||
sendAT("AT+CLVL=3", "OK", 1000);
|
||||
|
||||
// ---- Phase 3: Wait for network registration ----
|
||||
_state = ModemState::REGISTERING;
|
||||
MESH_DEBUG_PRINTLN("[Modem] waiting for network registration...");
|
||||
@@ -196,7 +568,6 @@ restart:
|
||||
for (int i = 0; i < 60; i++) { // up to 60 seconds
|
||||
if (sendAT("AT+CREG?", "OK", 2000)) {
|
||||
// Full response now in _atBuf, e.g.: "\r\n+CREG: 0,1\r\n\r\nOK\r\n"
|
||||
// stat: 1=registered home, 5=registered roaming
|
||||
char* p = strstr(_atBuf, "+CREG:");
|
||||
if (p) {
|
||||
int n, stat;
|
||||
@@ -215,12 +586,10 @@ restart:
|
||||
|
||||
if (!registered) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] registration timeout - continuing anyway");
|
||||
// Don't set ERROR; some networks are slow but SMS may still work
|
||||
}
|
||||
|
||||
// Query operator name
|
||||
if (sendAT("AT+COPS?", "OK", 5000)) {
|
||||
// +COPS: 0,0,"Operator Name",7
|
||||
char* p = strchr(_atBuf, '"');
|
||||
if (p) {
|
||||
p++;
|
||||
@@ -239,36 +608,33 @@ restart:
|
||||
pollCSQ();
|
||||
|
||||
// Sync ESP32 system clock from modem network time
|
||||
// Network time may take a few seconds to arrive after registration
|
||||
bool clockSet = false;
|
||||
for (int attempt = 0; attempt < 5 && !clockSet; attempt++) {
|
||||
if (attempt > 0) vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
if (sendAT("AT+CCLK?", "OK", 3000)) {
|
||||
// Response: +CCLK: "YY/MM/DD,HH:MM:SS±TZ" (TZ in quarter-hours)
|
||||
char* p = strstr(_atBuf, "+CCLK:");
|
||||
if (p) {
|
||||
int yy = 0, mo = 0, dd = 0, hh = 0, mm = 0, ss = 0, tz = 0;
|
||||
if (sscanf(p, "+CCLK: \"%d/%d/%d,%d:%d:%d", &yy, &mo, &dd, &hh, &mm, &ss) >= 6) {
|
||||
// Skip if modem clock not synced (default is 1970 = yy 70, or yy 0)
|
||||
if (yy < 24 || yy > 50) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] CCLK not synced yet (yy=%d), retrying...", yy);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse timezone offset (e.g. "+40" = UTC+10 in quarter-hours)
|
||||
char* tzp = p + 7; // skip "+CCLK: "
|
||||
// Parse timezone offset
|
||||
char* tzp = p + 7;
|
||||
while (*tzp && *tzp != '+' && *tzp != '-') tzp++;
|
||||
if (*tzp) tz = atoi(tzp);
|
||||
|
||||
struct tm t = {};
|
||||
t.tm_year = yy + 100; // years since 1900
|
||||
t.tm_mon = mo - 1; // 0-based
|
||||
t.tm_year = yy + 100;
|
||||
t.tm_mon = mo - 1;
|
||||
t.tm_mday = dd;
|
||||
t.tm_hour = hh;
|
||||
t.tm_min = mm;
|
||||
t.tm_sec = ss;
|
||||
time_t epoch = mktime(&t); // treats input as UTC (no TZ set on ESP32)
|
||||
epoch -= (tz * 15 * 60); // subtract local offset to get real UTC
|
||||
time_t epoch = mktime(&t);
|
||||
epoch -= (tz * 15 * 60);
|
||||
|
||||
struct timeval tv = { .tv_sec = epoch, .tv_usec = 0 };
|
||||
settimeofday(&tv, nullptr);
|
||||
@@ -284,7 +650,7 @@ restart:
|
||||
}
|
||||
|
||||
// Delete any stale SMS on SIM to free slots
|
||||
sendAT("AT+CMGD=1,4", "OK", 5000); // Delete all read messages
|
||||
sendAT("AT+CMGD=1,4", "OK", 5000);
|
||||
|
||||
_state = ModemState::READY;
|
||||
MESH_DEBUG_PRINTLN("[Modem] READY (CSQ=%d, operator=%s)", _csq, _operator);
|
||||
@@ -292,32 +658,128 @@ restart:
|
||||
// ---- Phase 4: Main loop ----
|
||||
unsigned long lastCSQPoll = 0;
|
||||
unsigned long lastSMSPoll = 0;
|
||||
const unsigned long CSQ_POLL_INTERVAL = 30000; // 30s
|
||||
const unsigned long SMS_POLL_INTERVAL = 10000; // 10s
|
||||
unsigned long lastCLCCPoll = 0;
|
||||
const unsigned long CSQ_POLL_INTERVAL = 30000; // 30s
|
||||
const unsigned long SMS_POLL_INTERVAL = 10000; // 10s
|
||||
const unsigned long CLCC_POLL_INTERVAL = 2000; // 2s (during dialing only)
|
||||
|
||||
while (true) {
|
||||
// Check for outgoing SMS in queue
|
||||
SMSOutgoing outMsg;
|
||||
if (xQueueReceive(_sendQueue, &outMsg, 0) == pdTRUE) {
|
||||
_state = ModemState::SENDING_SMS;
|
||||
bool ok = doSendSMS(outMsg.phone, outMsg.body);
|
||||
MESH_DEBUG_PRINTLN("[Modem] SMS send %s to %s", ok ? "OK" : "FAIL", outMsg.phone);
|
||||
_state = ModemState::READY;
|
||||
// ================================================================
|
||||
// Step 1: Drain URCs — catch RING, NO CARRIER, +CLIP, etc.
|
||||
// This must run every iteration to avoid missing time-sensitive
|
||||
// events like incoming calls or call-ended notifications.
|
||||
// ================================================================
|
||||
drainURCs();
|
||||
|
||||
// ================================================================
|
||||
// Step 2: Process call commands from main loop
|
||||
// ================================================================
|
||||
CallCommand callCmd;
|
||||
if (xQueueReceive(_callCmdQueue, &callCmd, 0) == pdTRUE) {
|
||||
switch (callCmd.cmd) {
|
||||
case CallCmd::DIAL:
|
||||
if (_state == ModemState::READY) {
|
||||
doDialCall(callCmd.phone);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("[Modem] Can't dial — state=%d", (int)_state);
|
||||
queueCallEvent(CallEventType::DIAL_FAILED, callCmd.phone);
|
||||
}
|
||||
break;
|
||||
|
||||
case CallCmd::ANSWER:
|
||||
if (_state == ModemState::RINGING_IN) {
|
||||
doAnswerCall();
|
||||
}
|
||||
break;
|
||||
|
||||
case CallCmd::HANGUP:
|
||||
if (isCallActive()) {
|
||||
doHangup();
|
||||
}
|
||||
break;
|
||||
|
||||
case CallCmd::DTMF:
|
||||
if (_state == ModemState::IN_CALL) {
|
||||
doSendDTMF(callCmd.dtmf);
|
||||
}
|
||||
break;
|
||||
|
||||
case CallCmd::SET_VOLUME:
|
||||
doSetVolume(callCmd.volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for incoming SMS periodically (not every loop iteration)
|
||||
if (millis() - lastSMSPoll > SMS_POLL_INTERVAL) {
|
||||
pollIncomingSMS();
|
||||
lastSMSPoll = millis();
|
||||
// ================================================================
|
||||
// Step 3: Poll AT+CLCC during DIALING as fallback.
|
||||
// Primary detection is via "VOICE CALL: BEGIN" URC (handled by
|
||||
// drainURCs/processURCLine above). CLCC polling is a safety net
|
||||
// in case the URC is missed or delayed.
|
||||
// ================================================================
|
||||
if (_state == ModemState::DIALING &&
|
||||
millis() - lastCLCCPoll > CLCC_POLL_INTERVAL) {
|
||||
if (sendAT("AT+CLCC", "OK", 2000)) {
|
||||
// +CLCC: 1,0,0,0,0,"number",129 — stat field:
|
||||
// 0=active, 1=held, 2=dialing, 3=alerting, 4=incoming, 5=waiting
|
||||
char* p = strstr(_atBuf, "+CLCC:");
|
||||
if (p) {
|
||||
int idx, dir, stat, mode, mpty;
|
||||
if (sscanf(p, "+CLCC: %d,%d,%d,%d,%d", &idx, &dir, &stat, &mode, &mpty) >= 3) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] CLCC: stat=%d", stat);
|
||||
if (stat == 0) {
|
||||
// Call is active — remote answered
|
||||
_state = ModemState::IN_CALL;
|
||||
_callStartTime = millis();
|
||||
queueCallEvent(CallEventType::CONNECTED, _callPhone);
|
||||
MESH_DEBUG_PRINTLN("[Modem] Call connected (detected via CLCC)");
|
||||
}
|
||||
// stat 2=dialing, 3=alerting — still setting up, keep polling
|
||||
}
|
||||
} else {
|
||||
// No +CLCC line in response — no active calls
|
||||
// This shouldn't happen during DIALING unless the call ended
|
||||
// and we missed the URC. Check state and clean up.
|
||||
// (NO CARRIER URC should have been caught by drainURCs)
|
||||
}
|
||||
}
|
||||
lastCLCCPoll = millis();
|
||||
}
|
||||
|
||||
// Periodic signal strength update
|
||||
// ================================================================
|
||||
// Step 4: SMS and signal polling (only when not in a call)
|
||||
// ================================================================
|
||||
if (!isCallActive()) {
|
||||
// Check for outgoing SMS in queue
|
||||
SMSOutgoing outMsg;
|
||||
if (xQueueReceive(_sendQueue, &outMsg, 0) == pdTRUE) {
|
||||
_state = ModemState::SENDING_SMS;
|
||||
bool ok = doSendSMS(outMsg.phone, outMsg.body);
|
||||
MESH_DEBUG_PRINTLN("[Modem] SMS send %s to %s", ok ? "OK" : "FAIL", outMsg.phone);
|
||||
_state = ModemState::READY;
|
||||
}
|
||||
|
||||
// Poll for incoming SMS periodically
|
||||
if (millis() - lastSMSPoll > SMS_POLL_INTERVAL) {
|
||||
pollIncomingSMS();
|
||||
lastSMSPoll = millis();
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic signal strength update (always, even during calls)
|
||||
if (millis() - lastCSQPoll > CSQ_POLL_INTERVAL) {
|
||||
pollCSQ();
|
||||
// Only poll CSQ if not actively in a call (avoid interrupting audio)
|
||||
if (!isCallActive()) {
|
||||
pollCSQ();
|
||||
}
|
||||
lastCSQPoll = millis();
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms loop — responsive for sends, calm for polls
|
||||
// Shorter delay during active call states for responsive URC handling
|
||||
if (isCallActive()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms — responsive to URCs
|
||||
} else {
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms — normal idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,8 +796,7 @@ bool ModemManager::modemPowerOn() {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
MESH_DEBUG_PRINTLN("[Modem] power supply enabled (GPIO %d HIGH)", MODEM_POWER_EN);
|
||||
|
||||
// Reset pulse — drive RST low briefly then release
|
||||
// (Some A7682E boards need this to clear stuck states)
|
||||
// Reset pulse
|
||||
pinMode(MODEM_RST, OUTPUT);
|
||||
digitalWrite(MODEM_RST, LOW);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
@@ -343,29 +804,23 @@ bool ModemManager::modemPowerOn() {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
MESH_DEBUG_PRINTLN("[Modem] reset pulse done (GPIO %d)", MODEM_RST);
|
||||
|
||||
// PWRKEY toggle: pull low for ≥1.5s then release
|
||||
// A7682E datasheet: PWRKEY low >1s triggers power-on
|
||||
// PWRKEY toggle
|
||||
pinMode(MODEM_PWRKEY, OUTPUT);
|
||||
digitalWrite(MODEM_PWRKEY, HIGH); // Start high (idle state)
|
||||
digitalWrite(MODEM_PWRKEY, HIGH);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
digitalWrite(MODEM_PWRKEY, LOW); // Active-low trigger
|
||||
digitalWrite(MODEM_PWRKEY, LOW);
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
digitalWrite(MODEM_PWRKEY, HIGH); // Release
|
||||
digitalWrite(MODEM_PWRKEY, HIGH);
|
||||
MESH_DEBUG_PRINTLN("[Modem] PWRKEY toggled, waiting for boot...");
|
||||
|
||||
// Wait for modem to boot — A7682E needs 3-5 seconds after PWRKEY
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
|
||||
// Assert DTR LOW — many cellular modems require DTR active (LOW) for AT mode
|
||||
// Assert DTR LOW
|
||||
pinMode(MODEM_DTR, OUTPUT);
|
||||
digitalWrite(MODEM_DTR, LOW);
|
||||
MESH_DEBUG_PRINTLN("[Modem] DTR asserted LOW (GPIO %d)", MODEM_DTR);
|
||||
|
||||
// Configure UART
|
||||
// NOTE: variant.h pin names are modem-perspective, so:
|
||||
// MODEM_RX (GPIO 10) = modem receives = ESP32 TX out
|
||||
// MODEM_TX (GPIO 11) = modem transmits = ESP32 RX in
|
||||
// Serial1.begin(baud, config, ESP32_RX, ESP32_TX)
|
||||
MODEM_SERIAL.begin(MODEM_BAUD, SERIAL_8N1, MODEM_TX, MODEM_RX);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
MESH_DEBUG_PRINTLN("[Modem] UART started (ESP32 RX=%d TX=%d @ %d)", MODEM_TX, MODEM_RX, MODEM_BAUD);
|
||||
@@ -373,7 +828,7 @@ bool ModemManager::modemPowerOn() {
|
||||
// Drain any boot garbage from UART
|
||||
while (MODEM_SERIAL.available()) MODEM_SERIAL.read();
|
||||
|
||||
// Test communication — generous attempts
|
||||
// Test communication
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] AT probe attempt %d/10", i + 1);
|
||||
if (sendAT("AT", "OK", 1500)) {
|
||||
@@ -392,14 +847,13 @@ bool ModemManager::modemPowerOn() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ModemManager::sendAT(const char* cmd, const char* expect, uint32_t timeout_ms) {
|
||||
// Flush any pending data
|
||||
while (MODEM_SERIAL.available()) MODEM_SERIAL.read();
|
||||
// Before flushing, drain any pending URCs so we don't lose them
|
||||
drainURCs();
|
||||
|
||||
Serial.printf("[Modem] TX: %s\n", cmd);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
bool ok = waitResponse(expect, timeout_ms, _atBuf, AT_BUF_SIZE);
|
||||
if (_atBuf[0]) {
|
||||
// Trim trailing whitespace for cleaner log output
|
||||
int len = strlen(_atBuf);
|
||||
while (len > 0 && (_atBuf[len-1] == '\r' || _atBuf[len-1] == '\n')) _atBuf[--len] = '\0';
|
||||
Serial.printf("[Modem] RX: %s [%s]\n", _atBuf, ok ? "OK" : "FAIL");
|
||||
@@ -427,6 +881,17 @@ bool ModemManager::waitResponse(const char* expect, uint32_t timeout_ms,
|
||||
if (buf && expect && strstr(buf, expect)) {
|
||||
return true;
|
||||
}
|
||||
// Also check for call-related URCs embedded in AT responses
|
||||
// (e.g. NO CARRIER can arrive during an AT+CLCC response)
|
||||
if (buf && strstr(buf, "NO CARRIER")) {
|
||||
processURCLine("NO CARRIER");
|
||||
}
|
||||
if (buf && strstr(buf, "BUSY")) {
|
||||
// Only process if we're in a call-related state
|
||||
if (_state == ModemState::DIALING) {
|
||||
processURCLine("BUSY");
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
@@ -457,23 +922,21 @@ void ModemManager::pollIncomingSMS() {
|
||||
char* p = _atBuf;
|
||||
while ((p = strstr(p, "+CMGL:")) != nullptr) {
|
||||
int idx;
|
||||
char stat[16], phone[SMS_PHONE_LEN], timestamp[24];
|
||||
|
||||
char phone[SMS_PHONE_LEN];
|
||||
|
||||
// Parse header line
|
||||
// +CMGL: 1,"REC UNREAD","+1234567890","","26/02/15,10:30:00+00"
|
||||
char* lineEnd = strchr(p, '\n');
|
||||
if (!lineEnd) break;
|
||||
|
||||
// Extract index
|
||||
if (sscanf(p, "+CMGL: %d", &idx) != 1) { p = lineEnd + 1; continue; }
|
||||
|
||||
// Extract phone number (between first and second quote pair after stat)
|
||||
char* q1 = strchr(p + 7, '"'); // skip "+CMGL: N,"
|
||||
// Extract phone number
|
||||
char* q1 = strchr(p + 7, '"');
|
||||
if (!q1) { p = lineEnd + 1; continue; }
|
||||
q1++; // skip opening quote of stat
|
||||
char* q2 = strchr(q1, '"'); // end of stat
|
||||
q1++;
|
||||
char* q2 = strchr(q1, '"');
|
||||
if (!q2) { p = lineEnd + 1; continue; }
|
||||
// Next quoted field is the phone number
|
||||
char* q3 = strchr(q2 + 1, '"');
|
||||
if (!q3) { p = lineEnd + 1; continue; }
|
||||
q3++;
|
||||
@@ -497,7 +960,7 @@ void ModemManager::pollIncomingSMS() {
|
||||
if (bodyLen >= SMS_BODY_LEN) bodyLen = SMS_BODY_LEN - 1;
|
||||
memcpy(incoming.body, p, bodyLen);
|
||||
incoming.body[bodyLen] = '\0';
|
||||
incoming.timestamp = (uint32_t)time(nullptr); // Real epoch from modem-synced clock
|
||||
incoming.timestamp = (uint32_t)time(nullptr);
|
||||
|
||||
// Queue for main loop
|
||||
xQueueSend(_recvQueue, &incoming, 0);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
// Runs AT commands on a dedicated FreeRTOS task (Core 0, priority 1) to never
|
||||
// block the mesh radio loop. Communicates with main loop via lock-free queues.
|
||||
//
|
||||
// Supports: SMS send/receive, voice call dial/answer/hangup/DTMF
|
||||
//
|
||||
// Guard: HAS_4G_MODEM (defined only for the 4G build environment)
|
||||
// =============================================================================
|
||||
|
||||
@@ -38,14 +40,18 @@
|
||||
|
||||
// Task configuration
|
||||
#define MODEM_TASK_PRIORITY 1 // Below mesh (default loop = priority 1 on core 1)
|
||||
#define MODEM_TASK_STACK_SIZE 4096
|
||||
#define MODEM_TASK_STACK_SIZE 6144 // Increased for call handling
|
||||
#define MODEM_TASK_CORE 0 // Run on core 0 (mesh runs on core 1)
|
||||
|
||||
// Queue sizes
|
||||
#define MODEM_SEND_QUEUE_SIZE 4
|
||||
#define MODEM_RECV_QUEUE_SIZE 8
|
||||
#define MODEM_CALL_CMD_QUEUE_SIZE 4
|
||||
#define MODEM_CALL_EVT_QUEUE_SIZE 4
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modem state machine
|
||||
// ---------------------------------------------------------------------------
|
||||
enum class ModemState {
|
||||
OFF,
|
||||
POWERING_ON,
|
||||
@@ -53,9 +59,17 @@ enum class ModemState {
|
||||
REGISTERING,
|
||||
READY,
|
||||
ERROR,
|
||||
SENDING_SMS
|
||||
SENDING_SMS,
|
||||
// Voice call states
|
||||
DIALING, // ATD sent, waiting for connect/carrier
|
||||
RINGING_IN, // Incoming call detected (RING URC)
|
||||
IN_CALL // Voice call active
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMS structures (unchanged)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Outgoing SMS (queued from main loop to modem task)
|
||||
struct SMSOutgoing {
|
||||
char phone[SMS_PHONE_LEN];
|
||||
@@ -69,28 +83,85 @@ struct SMSIncoming {
|
||||
uint32_t timestamp; // epoch seconds (from modem RTC or millis-based)
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Voice call structures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Commands from main loop → modem task
|
||||
enum class CallCmd : uint8_t {
|
||||
DIAL, // Initiate outgoing call
|
||||
ANSWER, // Answer incoming call
|
||||
HANGUP, // End active call or reject incoming
|
||||
DTMF, // Send DTMF tone during call
|
||||
SET_VOLUME // Set speaker volume
|
||||
};
|
||||
|
||||
struct CallCommand {
|
||||
CallCmd cmd;
|
||||
char phone[SMS_PHONE_LEN]; // Used by DIAL
|
||||
char dtmf; // Used by DTMF (single digit: 0-9, *, #)
|
||||
uint8_t volume; // Used by SET_VOLUME (0-5)
|
||||
};
|
||||
|
||||
// Events from modem task → main loop
|
||||
enum class CallEventType : uint8_t {
|
||||
INCOMING, // Incoming call ringing (+CLIP parsed)
|
||||
CONNECTED, // Call answered / outgoing connected
|
||||
ENDED, // Call ended (local hangup, remote hangup, or no carrier)
|
||||
MISSED, // Incoming call ended before answer
|
||||
BUSY, // Outgoing call got busy signal
|
||||
NO_ANSWER, // Outgoing call not answered
|
||||
DIAL_FAILED // ATD command failed
|
||||
};
|
||||
|
||||
struct CallEvent {
|
||||
CallEventType type;
|
||||
char phone[SMS_PHONE_LEN]; // Caller/callee number (from +CLIP or dial)
|
||||
uint32_t duration; // Call duration in seconds (for ENDED)
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ModemManager class
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class ModemManager {
|
||||
public:
|
||||
void begin();
|
||||
void shutdown();
|
||||
|
||||
// Non-blocking: queue an SMS for sending (returns false if queue full)
|
||||
// --- SMS API (unchanged) ---
|
||||
bool sendSMS(const char* phone, const char* body);
|
||||
|
||||
// Non-blocking: poll for received SMS (returns true if one was dequeued)
|
||||
bool recvSMS(SMSIncoming& out);
|
||||
|
||||
// State queries (lock-free reads)
|
||||
// --- Voice Call API ---
|
||||
bool dialCall(const char* phone); // Queue outgoing call
|
||||
bool answerCall(); // Answer incoming call
|
||||
bool hangupCall(); // End active / reject incoming
|
||||
bool sendDTMF(char digit); // Send DTMF during call
|
||||
bool setCallVolume(uint8_t level); // Set volume 0-5
|
||||
bool pollCallEvent(CallEvent& out); // Poll from main loop
|
||||
|
||||
// --- State queries (lock-free reads) ---
|
||||
ModemState getState() const { return _state; }
|
||||
int getSignalBars() const; // 0-5
|
||||
int getCSQ() const { return _csq; }
|
||||
bool isReady() const { return _state == ModemState::READY; }
|
||||
bool isInCall() const { return _state == ModemState::IN_CALL; }
|
||||
bool isRinging() const { return _state == ModemState::RINGING_IN; }
|
||||
bool isDialing() const { return _state == ModemState::DIALING; }
|
||||
bool isCallActive() const {
|
||||
return _state == ModemState::IN_CALL ||
|
||||
_state == ModemState::DIALING ||
|
||||
_state == ModemState::RINGING_IN;
|
||||
}
|
||||
const char* getOperator() const { return _operator; }
|
||||
const char* getCallPhone() const { return _callPhone; }
|
||||
uint32_t getCallStartTime() const { return _callStartTime; }
|
||||
|
||||
static const char* stateToString(ModemState s);
|
||||
|
||||
// Persistent enable/disable config (SD file /sms/modem.cfg)
|
||||
static bool loadEnabledConfig(); // returns true if enabled (default)
|
||||
static bool loadEnabledConfig();
|
||||
static void saveEnabledConfig(bool enabled);
|
||||
|
||||
private:
|
||||
@@ -98,11 +169,27 @@ private:
|
||||
volatile int _csq = 99; // 99 = unknown
|
||||
char _operator[24] = {0};
|
||||
|
||||
// Call state (written by modem task, read by main loop)
|
||||
char _callPhone[SMS_PHONE_LEN] = {0}; // Current call number
|
||||
volatile uint32_t _callStartTime = 0; // millis() when call connected
|
||||
|
||||
TaskHandle_t _taskHandle = nullptr;
|
||||
|
||||
// SMS queues
|
||||
QueueHandle_t _sendQueue = nullptr;
|
||||
QueueHandle_t _recvQueue = nullptr;
|
||||
|
||||
// Call queues
|
||||
QueueHandle_t _callCmdQueue = nullptr; // main loop → modem task
|
||||
QueueHandle_t _callEvtQueue = nullptr; // modem task → main loop
|
||||
|
||||
SemaphoreHandle_t _uartMutex = nullptr;
|
||||
|
||||
// URC line buffer (accumulated between AT commands)
|
||||
static const int URC_BUF_SIZE = 256;
|
||||
char _urcBuf[URC_BUF_SIZE];
|
||||
int _urcPos = 0;
|
||||
|
||||
// UART AT command helpers (called only from modem task)
|
||||
bool modemPowerOn();
|
||||
bool sendAT(const char* cmd, const char* expect, uint32_t timeout_ms = 2000);
|
||||
@@ -111,6 +198,18 @@ private:
|
||||
void pollIncomingSMS();
|
||||
bool doSendSMS(const char* phone, const char* body);
|
||||
|
||||
// URC (unsolicited result code) handling
|
||||
void drainURCs(); // Read available UART data, process complete lines
|
||||
void processURCLine(const char* line); // Handle a single URC line
|
||||
|
||||
// Call control (called from modem task)
|
||||
bool doDialCall(const char* phone);
|
||||
bool doAnswerCall();
|
||||
bool doHangup();
|
||||
bool doSendDTMF(char digit);
|
||||
bool doSetVolume(uint8_t level);
|
||||
void queueCallEvent(CallEventType type, const char* phone = nullptr, uint32_t duration = 0);
|
||||
|
||||
// FreeRTOS task
|
||||
static void taskEntry(void* param);
|
||||
void taskLoop();
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// SMSScreen - SMS messaging UI for T-Deck Pro (4G variant)
|
||||
// SMSScreen - SMS messaging & Voice Calls UI for T-Deck Pro (4G variant)
|
||||
//
|
||||
// Sub-views:
|
||||
// INBOX — list of conversations (names resolved via SMSContacts)
|
||||
// CONVERSATION — messages for a selected contact, scrollable
|
||||
// COMPOSE — text input for new SMS
|
||||
// CONTACTS — browsable contacts list, pick to compose
|
||||
// EDIT_CONTACT — add or edit a contact name for a phone number
|
||||
// INBOX — list of conversations (names resolved via SMSContacts)
|
||||
// CONVERSATION — messages for a selected contact, scrollable
|
||||
// COMPOSE — text input for new SMS
|
||||
// CONTACTS — browsable contacts list, pick to compose or call
|
||||
// EDIT_CONTACT — add or edit a contact name for a phone number
|
||||
// DIALING — outgoing call in progress
|
||||
// INCOMING_CALL — incoming call ringing (answer/reject)
|
||||
// IN_CALL — active voice call (timer, DTMF, volume, hangup)
|
||||
//
|
||||
// Navigation mirrors ChannelScreen conventions:
|
||||
// W/S: scroll Enter: select/send C: compose new/reply
|
||||
// Q: back Sh+Del: cancel compose
|
||||
// D: contacts (from inbox)
|
||||
// A: add/edit contact (from conversation)
|
||||
// F: call (from conversation or contacts)
|
||||
//
|
||||
// Guard: HAS_4G_MODEM
|
||||
// =============================================================================
|
||||
@@ -40,7 +44,11 @@ class UITask; // forward declaration
|
||||
|
||||
class SMSScreen : public UIScreen {
|
||||
public:
|
||||
enum SubView { INBOX, CONVERSATION, COMPOSE, CONTACTS, EDIT_CONTACT };
|
||||
enum SubView {
|
||||
INBOX, CONVERSATION, COMPOSE, CONTACTS, EDIT_CONTACT,
|
||||
// Voice call views
|
||||
DIALING, INCOMING_CALL, IN_CALL
|
||||
};
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
@@ -77,8 +85,14 @@ private:
|
||||
char _editPhone[SMS_PHONE_LEN];
|
||||
char _editNameBuf[SMS_CONTACT_NAME_LEN];
|
||||
int _editNamePos;
|
||||
bool _editIsNew; // true = adding new, false = editing existing
|
||||
SubView _editReturnView; // where to return after save/cancel
|
||||
bool _editIsNew;
|
||||
SubView _editReturnView;
|
||||
|
||||
// Voice call state
|
||||
char _callPhone[SMS_PHONE_LEN]; // Number for current/pending call
|
||||
unsigned long _callConnectedMillis; // millis() when call connected
|
||||
SubView _preCallView; // View to return to after call ends
|
||||
uint8_t _callVolume; // Current volume level 0-5
|
||||
|
||||
// Refresh debounce
|
||||
bool _needsRefresh;
|
||||
@@ -95,10 +109,19 @@ private:
|
||||
|
||||
void refreshConversation() {
|
||||
_msgCount = smsStore.loadMessages(_activePhone, _msgs, SMS_MSG_PAGE_SIZE);
|
||||
// Scroll to bottom (newest messages are at end now, chat-style)
|
||||
_msgScrollPos = (_msgCount > 3) ? _msgCount - 3 : 0;
|
||||
}
|
||||
|
||||
// Helper: initiate a call to a phone number
|
||||
void startCall(const char* phone) {
|
||||
strncpy(_callPhone, phone, SMS_PHONE_LEN - 1);
|
||||
_callPhone[SMS_PHONE_LEN - 1] = '\0';
|
||||
_callConnectedMillis = 0;
|
||||
_preCallView = _view;
|
||||
modemManager.dialCall(phone);
|
||||
_view = DIALING;
|
||||
}
|
||||
|
||||
public:
|
||||
SMSScreen(UITask* task)
|
||||
: _task(task), _view(INBOX)
|
||||
@@ -108,6 +131,7 @@ public:
|
||||
, _phoneInputPos(0), _enteringPhone(false)
|
||||
, _contactsCursor(0), _contactsScrollTop(0)
|
||||
, _editNamePos(0), _editIsNew(false), _editReturnView(INBOX)
|
||||
, _callConnectedMillis(0), _preCallView(INBOX), _callVolume(3)
|
||||
, _needsRefresh(false), _lastRefresh(0)
|
||||
, _sdReady(false)
|
||||
{
|
||||
@@ -117,6 +141,7 @@ public:
|
||||
memset(_activePhone, 0, sizeof(_activePhone));
|
||||
memset(_editPhone, 0, sizeof(_editPhone));
|
||||
memset(_editNameBuf, 0, sizeof(_editNameBuf));
|
||||
memset(_callPhone, 0, sizeof(_callPhone));
|
||||
}
|
||||
|
||||
void setSDReady(bool ready) { _sdReady = ready; }
|
||||
@@ -131,8 +156,11 @@ public:
|
||||
SubView getSubView() const { return _view; }
|
||||
bool isComposing() const { return _view == COMPOSE; }
|
||||
bool isEnteringPhone() const { return _enteringPhone; }
|
||||
bool isInCallView() const {
|
||||
return _view == DIALING || _view == INCOMING_CALL || _view == IN_CALL;
|
||||
}
|
||||
|
||||
// Called from main loop when an SMS arrives (saves to store + refreshes)
|
||||
// Called from main loop when an SMS arrives
|
||||
void onIncomingSMS(const char* phone, const char* body, uint32_t timestamp) {
|
||||
if (_sdReady) {
|
||||
smsStore.saveMessage(phone, body, false, timestamp);
|
||||
@@ -146,6 +174,47 @@ public:
|
||||
_needsRefresh = true;
|
||||
}
|
||||
|
||||
// Called from main loop when a call event arrives
|
||||
void onCallEvent(const CallEvent& evt) {
|
||||
switch (evt.type) {
|
||||
case CallEventType::INCOMING:
|
||||
// Incoming call — switch to incoming call view
|
||||
strncpy(_callPhone, evt.phone, SMS_PHONE_LEN - 1);
|
||||
_callPhone[SMS_PHONE_LEN - 1] = '\0';
|
||||
if (_view != INCOMING_CALL) {
|
||||
_preCallView = _view;
|
||||
_view = INCOMING_CALL;
|
||||
}
|
||||
break;
|
||||
|
||||
case CallEventType::CONNECTED:
|
||||
// Call connected — switch to in-call view
|
||||
_callConnectedMillis = millis();
|
||||
_view = IN_CALL;
|
||||
break;
|
||||
|
||||
case CallEventType::ENDED:
|
||||
case CallEventType::MISSED:
|
||||
case CallEventType::BUSY:
|
||||
case CallEventType::NO_ANSWER:
|
||||
case CallEventType::DIAL_FAILED:
|
||||
// Call ended — return to previous view
|
||||
_callPhone[0] = '\0';
|
||||
_callConnectedMillis = 0;
|
||||
// Return to pre-call view or inbox
|
||||
if (_preCallView == DIALING || _preCallView == INCOMING_CALL || _preCallView == IN_CALL) {
|
||||
_view = INBOX;
|
||||
if (_sdReady) refreshInbox();
|
||||
} else {
|
||||
_view = _preCallView;
|
||||
if (_view == INBOX && _sdReady) refreshInbox();
|
||||
if (_view == CONVERSATION) refreshConversation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
_needsRefresh = true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Signal strength indicator (top-right corner)
|
||||
// =========================================================================
|
||||
@@ -154,7 +223,6 @@ public:
|
||||
ModemState ms = modemManager.getState();
|
||||
int bars = modemManager.getSignalBars();
|
||||
|
||||
// Draw signal bars (4 bars, increasing height)
|
||||
int barWidth = 3;
|
||||
int barGap = 2;
|
||||
int maxBarH = 10;
|
||||
@@ -174,8 +242,9 @@ public:
|
||||
x += barWidth + barGap;
|
||||
}
|
||||
|
||||
// Show modem state text if not ready
|
||||
if (ms != ModemState::READY && ms != ModemState::SENDING_SMS) {
|
||||
if (ms != ModemState::READY && ms != ModemState::SENDING_SMS &&
|
||||
ms != ModemState::DIALING && ms != ModemState::IN_CALL &&
|
||||
ms != ModemState::RINGING_IN) {
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
const char* label = ModemManager::stateToString(ms);
|
||||
@@ -197,11 +266,14 @@ public:
|
||||
_lastRefresh = millis();
|
||||
|
||||
switch (_view) {
|
||||
case INBOX: return renderInbox(display);
|
||||
case CONVERSATION: return renderConversation(display);
|
||||
case COMPOSE: return renderCompose(display);
|
||||
case CONTACTS: return renderContacts(display);
|
||||
case EDIT_CONTACT: return renderEditContact(display);
|
||||
case INBOX: return renderInbox(display);
|
||||
case CONVERSATION: return renderConversation(display);
|
||||
case COMPOSE: return renderCompose(display);
|
||||
case CONTACTS: return renderContacts(display);
|
||||
case EDIT_CONTACT: return renderEditContact(display);
|
||||
case DIALING: return renderDialing(display);
|
||||
case INCOMING_CALL: return renderIncomingCall(display);
|
||||
case IN_CALL: return renderInCall(display);
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
@@ -246,7 +318,6 @@ public:
|
||||
int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2);
|
||||
if (visibleCount < 1) visibleCount = 1;
|
||||
|
||||
// Adjust scroll to keep cursor visible
|
||||
if (_inboxCursor < _inboxScrollTop) _inboxScrollTop = _inboxCursor;
|
||||
if (_inboxCursor >= _inboxScrollTop + visibleCount) {
|
||||
_inboxScrollTop = _inboxCursor - visibleCount + 1;
|
||||
@@ -259,7 +330,6 @@ public:
|
||||
|
||||
bool selected = (idx == _inboxCursor);
|
||||
|
||||
// Resolve contact name (shows name if saved, phone otherwise)
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(c.phone, dispName, sizeof(dispName));
|
||||
|
||||
@@ -307,7 +377,6 @@ public:
|
||||
|
||||
// ---- Conversation view ----
|
||||
int renderConversation(DisplayDriver& display) {
|
||||
// Header - show contact name if available, phone otherwise
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
@@ -315,7 +384,6 @@ public:
|
||||
smsContacts.displayName(_activePhone, convTitle, sizeof(convTitle));
|
||||
display.print(convTitle);
|
||||
|
||||
// Signal icon
|
||||
renderSignalIndicator(display, display.width() - 2, 0);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -333,7 +401,6 @@ public:
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
|
||||
// Estimate chars per line
|
||||
uint16_t testWidth = display.getTextWidth("MMMMMMMMMM");
|
||||
int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20;
|
||||
if (charsPerLine < 12) charsPerLine = 12;
|
||||
@@ -346,15 +413,13 @@ public:
|
||||
SMSMessage& msg = _msgs[i];
|
||||
if (!msg.valid) continue;
|
||||
|
||||
// Direction indicator
|
||||
display.setCursor(0, y);
|
||||
display.setColor(msg.isSent ? DisplayDriver::BLUE : DisplayDriver::YELLOW);
|
||||
|
||||
// Time formatting (epoch-aware)
|
||||
char timeStr[16];
|
||||
time_t now = time(nullptr);
|
||||
bool haveEpoch = (now > 1700000000); // system clock is set
|
||||
bool msgIsEpoch = (msg.timestamp > 1700000000); // msg has real timestamp
|
||||
bool haveEpoch = (now > 1700000000);
|
||||
bool msgIsEpoch = (msg.timestamp > 1700000000);
|
||||
|
||||
if (haveEpoch && msgIsEpoch) {
|
||||
uint32_t age = (uint32_t)(now - msg.timestamp);
|
||||
@@ -372,7 +437,6 @@ public:
|
||||
display.print(header);
|
||||
y += lineHeight;
|
||||
|
||||
// Message body with simple word wrap
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
int textLen = strlen(msg.body);
|
||||
int pos = 0;
|
||||
@@ -402,13 +466,16 @@ public:
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
// Footer
|
||||
// Footer — now includes F:Call
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Bk A:Add Contact");
|
||||
display.print("Q:Bk A:Ct");
|
||||
const char* mid = "F:Call";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
const char* rt = "C:Reply";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
@@ -428,7 +495,6 @@ public:
|
||||
display.print(_phoneInputBuf);
|
||||
display.print("_");
|
||||
} else {
|
||||
// Show contact name if available
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(_composePhone, dispName, sizeof(dispName));
|
||||
char toLabel[40];
|
||||
@@ -440,7 +506,6 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
if (!_enteringPhone) {
|
||||
// Message body
|
||||
display.setCursor(0, 14);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(0);
|
||||
@@ -463,7 +528,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor
|
||||
display.setCursor(x * (display.width() / charsPerLine), y);
|
||||
display.print("_");
|
||||
display.setTextSize(1);
|
||||
@@ -523,7 +587,6 @@ public:
|
||||
int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2);
|
||||
if (visibleCount < 1) visibleCount = 1;
|
||||
|
||||
// Adjust scroll
|
||||
if (_contactsCursor >= cnt) _contactsCursor = cnt - 1;
|
||||
if (_contactsCursor < 0) _contactsCursor = 0;
|
||||
if (_contactsCursor < _contactsScrollTop) _contactsScrollTop = _contactsCursor;
|
||||
@@ -538,14 +601,12 @@ public:
|
||||
|
||||
bool selected = (idx == _contactsCursor);
|
||||
|
||||
// Name
|
||||
display.setCursor(0, y);
|
||||
display.setColor(selected ? DisplayDriver::GREEN : DisplayDriver::LIGHT);
|
||||
if (selected) display.print("> ");
|
||||
display.print(ct.name);
|
||||
y += lineHeight;
|
||||
|
||||
// Phone (dimmer)
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(12, y);
|
||||
display.print(ct.phone);
|
||||
@@ -554,13 +615,16 @@ public:
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
// Footer
|
||||
// Footer — now includes F:Call
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Back");
|
||||
const char* mid = "F:Call";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
const char* rt = "Ent:SMS";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
@@ -578,14 +642,12 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// Phone number (read-only)
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 16);
|
||||
display.print("Phone: ");
|
||||
display.print(_editPhone);
|
||||
|
||||
// Name input
|
||||
display.setCursor(0, 30);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("Name: ");
|
||||
@@ -595,7 +657,6 @@ public:
|
||||
|
||||
display.setTextSize(1);
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
@@ -609,17 +670,210 @@ public:
|
||||
return 2000;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// VOICE CALL RENDER VIEWS
|
||||
// =========================================================================
|
||||
|
||||
// ---- Dialing (outgoing call in progress) ----
|
||||
int renderDialing(DisplayDriver& display) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
display.print("Calling...");
|
||||
|
||||
renderSignalIndicator(display, display.width() - 2, 0);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// Contact name / phone number centred
|
||||
int centreY = display.height() / 2 - 20;
|
||||
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(_callPhone, dispName, sizeof(dispName));
|
||||
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t nameW = display.getTextWidth(dispName);
|
||||
display.setCursor((display.width() - nameW) / 2, centreY);
|
||||
display.print(dispName);
|
||||
|
||||
// Show raw phone number below name (if name differs from phone)
|
||||
if (strcmp(dispName, _callPhone) != 0) {
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t phoneW = display.getTextWidth(_callPhone);
|
||||
display.setCursor((display.width() - phoneW) / 2, centreY + 16);
|
||||
display.print(_callPhone);
|
||||
}
|
||||
|
||||
// Animated dots indicator
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
unsigned long elapsed = millis() / 500;
|
||||
int dots = (elapsed % 4);
|
||||
char dotStr[5] = " ";
|
||||
for (int i = 0; i < dots; i++) dotStr[i] = '.';
|
||||
dotStr[dots] = '\0';
|
||||
uint16_t dotW = display.getTextWidth("...");
|
||||
display.setCursor((display.width() - dotW) / 2, centreY + 32);
|
||||
display.print(dotStr);
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
const char* hangup = "Ent:Hangup";
|
||||
display.setCursor((display.width() - display.getTextWidth(hangup)) / 2, footerY);
|
||||
display.print(hangup);
|
||||
|
||||
return 500; // Fast refresh for animated dots
|
||||
}
|
||||
|
||||
// ---- Incoming call ----
|
||||
int renderIncomingCall(DisplayDriver& display) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
display.print("Incoming Call");
|
||||
|
||||
renderSignalIndicator(display, display.width() - 2, 0);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
int centreY = display.height() / 2 - 20;
|
||||
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
if (_callPhone[0]) {
|
||||
smsContacts.displayName(_callPhone, dispName, sizeof(dispName));
|
||||
} else {
|
||||
strncpy(dispName, "Unknown", sizeof(dispName));
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t nameW = display.getTextWidth(dispName);
|
||||
display.setCursor((display.width() - nameW) / 2, centreY);
|
||||
display.print(dispName);
|
||||
|
||||
if (_callPhone[0] && strcmp(dispName, _callPhone) != 0) {
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t phoneW = display.getTextWidth(_callPhone);
|
||||
display.setCursor((display.width() - phoneW) / 2, centreY + 16);
|
||||
display.print(_callPhone);
|
||||
}
|
||||
|
||||
// Ringing indicator
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
unsigned long elapsed = millis() / 300;
|
||||
const char* ring = (elapsed % 2 == 0) ? "RINGING" : "";
|
||||
uint16_t ringW = display.getTextWidth("RINGING");
|
||||
display.setCursor((display.width() - ringW) / 2, centreY + 36);
|
||||
display.print(ring);
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Ent:Answer");
|
||||
const char* rt = "Q:Reject";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
|
||||
return 500; // Fast refresh for flashing ring indicator
|
||||
}
|
||||
|
||||
// ---- In-call ----
|
||||
int renderInCall(DisplayDriver& display) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
display.print("In Call");
|
||||
|
||||
renderSignalIndicator(display, display.width() - 2, 0);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
int centreY = 20;
|
||||
|
||||
// Contact name
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(_callPhone, dispName, sizeof(dispName));
|
||||
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t nameW = display.getTextWidth(dispName);
|
||||
display.setCursor((display.width() - nameW) / 2, centreY);
|
||||
display.print(dispName);
|
||||
|
||||
// Phone number (if name differs)
|
||||
if (strcmp(dispName, _callPhone) != 0) {
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t phoneW = display.getTextWidth(_callPhone);
|
||||
display.setCursor((display.width() - phoneW) / 2, centreY + 16);
|
||||
display.print(_callPhone);
|
||||
}
|
||||
|
||||
// Call duration timer
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
uint32_t durSec = 0;
|
||||
if (_callConnectedMillis > 0) {
|
||||
durSec = (millis() - _callConnectedMillis) / 1000;
|
||||
}
|
||||
char timerStr[12];
|
||||
snprintf(timerStr, sizeof(timerStr), "%02d:%02d", (int)(durSec / 60), (int)(durSec % 60));
|
||||
uint16_t timerW = display.getTextWidth(timerStr);
|
||||
display.setCursor((display.width() - timerW) / 2, centreY + 40);
|
||||
display.print(timerStr);
|
||||
|
||||
// Volume indicator
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char volStr[16];
|
||||
snprintf(volStr, sizeof(volStr), "Vol: %d/5", _callVolume);
|
||||
display.setCursor(0, centreY + 60);
|
||||
display.print(volStr);
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Ent:Hang");
|
||||
const char* mid = "W/S:Vol";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
const char* rt = "0-9:DTMF";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
|
||||
return 1000; // 1s refresh for call timer
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// INPUT HANDLING
|
||||
// =========================================================================
|
||||
|
||||
bool handleInput(char c) override {
|
||||
switch (_view) {
|
||||
case INBOX: return handleInboxInput(c);
|
||||
case CONVERSATION: return handleConversationInput(c);
|
||||
case COMPOSE: return handleComposeInput(c);
|
||||
case CONTACTS: return handleContactsInput(c);
|
||||
case EDIT_CONTACT: return handleEditContactInput(c);
|
||||
case INBOX: return handleInboxInput(c);
|
||||
case CONVERSATION: return handleConversationInput(c);
|
||||
case COMPOSE: return handleComposeInput(c);
|
||||
case CONTACTS: return handleContactsInput(c);
|
||||
case EDIT_CONTACT: return handleEditContactInput(c);
|
||||
case DIALING: return handleDialingInput(c);
|
||||
case INCOMING_CALL: return handleIncomingCallInput(c);
|
||||
case IN_CALL: return handleInCallInput(c);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -687,6 +941,12 @@ public:
|
||||
_view = COMPOSE;
|
||||
return true;
|
||||
|
||||
case 'f': case 'F': // Call this contact
|
||||
if (modemManager.isReady() && _activePhone[0]) {
|
||||
startCall(_activePhone);
|
||||
}
|
||||
return true;
|
||||
|
||||
case 'a': case 'A': { // Add/edit contact for this number
|
||||
strncpy(_editPhone, _activePhone, SMS_PHONE_LEN - 1);
|
||||
_editPhone[SMS_PHONE_LEN - 1] = '\0';
|
||||
@@ -828,6 +1088,13 @@ public:
|
||||
}
|
||||
return true;
|
||||
|
||||
case 'f': case 'F': // Call selected contact
|
||||
if (cnt > 0 && _contactsCursor < cnt && modemManager.isReady()) {
|
||||
const SMSContact& ct = smsContacts.get(_contactsCursor);
|
||||
startCall(ct.phone);
|
||||
}
|
||||
return true;
|
||||
|
||||
case 'q': case 'Q': // Back to inbox
|
||||
refreshInbox();
|
||||
_view = INBOX;
|
||||
@@ -879,6 +1146,70 @@ public:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// VOICE CALL INPUT HANDLERS
|
||||
// =========================================================================
|
||||
|
||||
// ---- Dialing input ----
|
||||
bool handleDialingInput(char c) {
|
||||
switch (c) {
|
||||
case '\r': // Enter — hangup / cancel dial
|
||||
case 'q': case 'Q':
|
||||
modemManager.hangupCall();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true; // Absorb all keys during dialing
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Incoming call input ----
|
||||
bool handleIncomingCallInput(char c) {
|
||||
switch (c) {
|
||||
case '\r': // Enter — answer call
|
||||
modemManager.answerCall();
|
||||
return true;
|
||||
|
||||
case 'q': case 'Q': // Reject call
|
||||
modemManager.hangupCall();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true; // Absorb all keys
|
||||
}
|
||||
}
|
||||
|
||||
// ---- In-call input ----
|
||||
bool handleInCallInput(char c) {
|
||||
switch (c) {
|
||||
case '\r': // Enter — hangup
|
||||
case 'q': case 'Q':
|
||||
modemManager.hangupCall();
|
||||
return true;
|
||||
|
||||
case 'w': case 'W': // Volume up
|
||||
if (_callVolume < 5) {
|
||||
_callVolume++;
|
||||
modemManager.setCallVolume(_callVolume);
|
||||
}
|
||||
return true;
|
||||
|
||||
case 's': case 'S': // Volume down
|
||||
if (_callVolume > 0) {
|
||||
_callVolume--;
|
||||
modemManager.setCallVolume(_callVolume);
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
// 0-9, *, # — send as DTMF
|
||||
if ((c >= '0' && c <= '9') || c == '*' || c == '#') {
|
||||
modemManager.sendDTMF(c);
|
||||
}
|
||||
return true; // Absorb all keys during call
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SMS_SCREEN_H
|
||||
|
||||
Reference in New Issue
Block a user