Files
pyxis/lib/tdeck_ui/UI/LXMF/CallScreen.cpp
torlando-tech f73fbb2568 Fix LXST voice call interop with Python LXST/Columba
- Fix thread safety: defer Reticulum link callbacks (packet, link_closed)
  to call_update() which runs under LVGL lock, preventing crashes from
  concurrent LVGL access across cores
- Fix outgoing call signal handling: store link reference and re-register
  packet/link_closed callbacks in on_call_link_established so signals
  are actually received
- Fix call answer screen freeze: update UI before blocking audio init
  (I2S/ES7210/Codec2 setup) so screen renders immediately
- Fix audio direction: use startPlayback() (speaker RX) instead of
  startCapture() (mic TX) so received audio is actually heard
- Add msgpack wire format for LXST signalling and audio frames
- Add LXST IN destination for receiving calls + announce support
- Add incoming call UI (Answer/Reject buttons) on CallScreen
- Add path request before outgoing call link establishment
- Add LXST announce handler registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:00:07 -05:00

232 lines
8.5 KiB
C++

// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#include "CallScreen.h"
#ifdef ARDUINO
#include "Theme.h"
#include "../LVGL/LVGLInit.h"
namespace UI {
namespace LXMF {
CallScreen::CallScreen(lv_obj_t* parent)
: _screen(nullptr), _label_state(nullptr), _label_peer(nullptr),
_label_duration(nullptr), _btn_mute(nullptr), _label_mute(nullptr),
_btn_hangup(nullptr), _muted(false), _state(CallState::CONNECTING) {
_screen = lv_obj_create(parent ? parent : lv_scr_act());
lv_obj_set_size(_screen, 320, 240);
lv_obj_set_style_bg_color(_screen, Theme::surface(), 0);
lv_obj_set_style_bg_opa(_screen, LV_OPA_COVER, 0);
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_pad_all(_screen, 0, 0);
create_ui();
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
}
CallScreen::~CallScreen() {
if (_screen) {
lv_obj_del(_screen);
}
}
void CallScreen::create_ui() {
// State label (top area) - "Connecting...", "Ringing...", "In Call", "Call Ended"
_label_state = lv_label_create(_screen);
lv_label_set_text(_label_state, "Connecting...");
lv_obj_set_style_text_color(_label_state, Theme::textSecondary(), 0);
lv_obj_set_style_text_font(_label_state, &lv_font_montserrat_16, 0);
lv_obj_align(_label_state, LV_ALIGN_TOP_MID, 0, 30);
// Peer name/hash label
_label_peer = lv_label_create(_screen);
lv_label_set_text(_label_peer, "");
lv_obj_set_style_text_color(_label_peer, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(_label_peer, &lv_font_montserrat_14, 0);
lv_obj_set_width(_label_peer, 280);
lv_obj_set_style_text_align(_label_peer, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_long_mode(_label_peer, LV_LABEL_LONG_DOT);
lv_obj_align(_label_peer, LV_ALIGN_TOP_MID, 0, 60);
// Duration timer label
_label_duration = lv_label_create(_screen);
lv_label_set_text(_label_duration, "");
lv_obj_set_style_text_color(_label_duration, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(_label_duration, &lv_font_montserrat_16, 0);
lv_obj_align(_label_duration, LV_ALIGN_CENTER, 0, -10);
// Button row at bottom
// Mute button (left)
_btn_mute = lv_btn_create(_screen);
lv_obj_set_size(_btn_mute, 100, 44);
lv_obj_align(_btn_mute, LV_ALIGN_BOTTOM_LEFT, 30, -30);
lv_obj_set_style_bg_color(_btn_mute, Theme::btnSecondary(), 0);
lv_obj_set_style_bg_color(_btn_mute, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
lv_obj_set_style_bg_color(_btn_mute, Theme::surfaceElevated(), LV_STATE_FOCUSED);
lv_obj_set_style_radius(_btn_mute, 8, 0);
lv_obj_add_event_cb(_btn_mute, on_mute_clicked, LV_EVENT_CLICKED, this);
_label_mute = lv_label_create(_btn_mute);
lv_label_set_text(_label_mute, LV_SYMBOL_AUDIO " Mute");
lv_obj_set_style_text_color(_label_mute, Theme::textPrimary(), 0);
lv_obj_center(_label_mute);
// Hangup button (right) - red
_btn_hangup = lv_btn_create(_screen);
lv_obj_set_size(_btn_hangup, 100, 44);
lv_obj_align(_btn_hangup, LV_ALIGN_BOTTOM_RIGHT, -30, -30);
lv_obj_set_style_bg_color(_btn_hangup, lv_color_hex(0xC62828), 0); // Dark red
lv_obj_set_style_bg_color(_btn_hangup, lv_color_hex(0xB71C1C), LV_STATE_PRESSED);
lv_obj_set_style_bg_color(_btn_hangup, lv_color_hex(0xD32F2F), LV_STATE_FOCUSED);
lv_obj_set_style_radius(_btn_hangup, 8, 0);
lv_obj_add_event_cb(_btn_hangup, on_hangup_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* label_hangup = lv_label_create(_btn_hangup);
lv_label_set_text(label_hangup, LV_SYMBOL_CLOSE " End");
lv_obj_set_style_text_color(label_hangup, lv_color_white(), 0);
lv_obj_center(label_hangup);
}
void CallScreen::set_peer(const RNS::Bytes& peer_hash, const String& display_name) {
if (display_name.length() > 0) {
char buf[64];
snprintf(buf, sizeof(buf), "%s\n%.16s...", display_name.c_str(),
peer_hash.toHex().c_str());
lv_label_set_text(_label_peer, buf);
} else {
lv_label_set_text(_label_peer, peer_hash.toHex().c_str());
}
}
void CallScreen::set_state(CallState state) {
_state = state;
switch (state) {
case CallState::CONNECTING:
lv_label_set_text(_label_state, "Connecting...");
lv_label_set_text(_label_duration, "");
break;
case CallState::RINGING:
lv_label_set_text(_label_state, "Ringing...");
break;
case CallState::INCOMING_RINGING: {
lv_label_set_text(_label_state, LV_SYMBOL_CALL " Incoming Call");
lv_label_set_text(_label_duration, "");
// Repurpose mute button as Answer
lv_label_set_text(_label_mute, LV_SYMBOL_OK " Answer");
lv_obj_set_style_bg_color(_btn_mute, lv_color_hex(0x2E7D32), 0); // Green
// Change hangup label to Reject
lv_obj_t* hangup_label = lv_obj_get_child(_btn_hangup, 0);
if (hangup_label) lv_label_set_text(hangup_label, LV_SYMBOL_CLOSE " Reject");
break;
}
case CallState::ACTIVE: {
lv_label_set_text(_label_state, LV_SYMBOL_CALL " In Call");
lv_obj_set_style_text_color(_label_state, Theme::success(), 0);
// Restore button labels (may have been Answer/Reject for incoming)
lv_label_set_text(_label_mute, _muted ? LV_SYMBOL_MUTE " Muted" : LV_SYMBOL_AUDIO " Mute");
lv_obj_set_style_bg_color(_btn_mute, _muted ? Theme::warning() : Theme::btnSecondary(), 0);
lv_obj_t* hangup_label = lv_obj_get_child(_btn_hangup, 0);
if (hangup_label) lv_label_set_text(hangup_label, LV_SYMBOL_CLOSE " End");
break;
}
case CallState::ENDED:
lv_label_set_text(_label_state, "Call Ended");
lv_obj_set_style_text_color(_label_state, Theme::textSecondary(), 0);
break;
}
}
void CallScreen::set_duration(uint32_t seconds) {
char buf[16];
uint32_t m = seconds / 60;
uint32_t s = seconds % 60;
snprintf(buf, sizeof(buf), "%02lu:%02lu", (unsigned long)m, (unsigned long)s);
lv_label_set_text(_label_duration, buf);
}
void CallScreen::set_muted(bool muted) {
_muted = muted;
// Don't change button appearance during incoming call (button shows "Answer")
if (_state == CallState::INCOMING_RINGING) return;
if (muted) {
lv_label_set_text(_label_mute, LV_SYMBOL_MUTE " Muted");
lv_obj_set_style_bg_color(_btn_mute, Theme::warning(), 0);
} else {
lv_label_set_text(_label_mute, LV_SYMBOL_AUDIO " Mute");
lv_obj_set_style_bg_color(_btn_mute, Theme::btnSecondary(), 0);
}
}
void CallScreen::set_hangup_callback(HangupCallback callback) {
_hangup_callback = callback;
}
void CallScreen::set_mute_callback(MuteCallback callback) {
_mute_callback = callback;
}
void CallScreen::set_answer_callback(AnswerCallback callback) {
_answer_callback = callback;
}
void CallScreen::show() {
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(_screen);
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
if (_btn_mute) lv_group_add_obj(group, _btn_mute);
if (_btn_hangup) lv_group_add_obj(group, _btn_hangup);
// Focus answer button for incoming calls, hangup for outgoing
if (_state == CallState::INCOMING_RINGING && _btn_mute) {
lv_group_focus_obj(_btn_mute);
} else {
lv_group_focus_obj(_btn_hangup);
}
}
}
void CallScreen::hide() {
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
if (_btn_mute) lv_group_remove_obj(_btn_mute);
if (_btn_hangup) lv_group_remove_obj(_btn_hangup);
}
}
lv_obj_t* CallScreen::get_object() {
return _screen;
}
void CallScreen::on_hangup_clicked(lv_event_t* event) {
auto* self = static_cast<CallScreen*>(lv_event_get_user_data(event));
if (self && self->_hangup_callback) {
self->_hangup_callback();
}
}
void CallScreen::on_mute_clicked(lv_event_t* event) {
auto* self = static_cast<CallScreen*>(lv_event_get_user_data(event));
if (!self) return;
// In incoming call state, this button acts as "Answer"
if (self->_state == CallState::INCOMING_RINGING && self->_answer_callback) {
self->_answer_callback();
return;
}
if (self->_mute_callback) {
self->_muted = !self->_muted;
self->set_muted(self->_muted);
self->_mute_callback(self->_muted);
}
}
} // namespace LXMF
} // namespace UI
#endif // ARDUINO