Add ringtones/wallpapers plugins, AT console, dialer USSD/*#06#, and SIM icon fix

This commit is contained in:
Luxferre
2026-10-24 19:58:11 +03:00
parent e549aaf413
commit dc59704547
64 changed files with 2743 additions and 130 deletions
+287 -35
View File
@@ -29,7 +29,8 @@ void handleSignal(int) {
enum class TabId {
SimCards = 0,
RadioInfo = 1,
ModemLog = 2,
AtConsole = 2,
ModemLog = 3,
};
enum class ModalMode {
@@ -94,6 +95,8 @@ struct ModemStatusData {
std::string accessTech;
int signalQuality = 0;
std::string imei;
std::string imei2;
std::string imeiSv;
std::string ownNumber;
std::string simDbusPath;
std::string simIccid;
@@ -126,18 +129,42 @@ struct ModemLogRow {
std::string detail;
};
std::string shellQuote(const std::string& s) {
std::string out = "'";
for (char c : s) {
if (c == '\'') {
out += "'\\''";
} else {
out.push_back(c);
}
}
out.push_back('\'');
return out;
}
constexpr const char* kAtPresets[] = {
"AT",
"ATI",
"AT+CGSN",
"AT+CSQ",
"AT+CREG?",
"AT+COPS?",
"AT+CPIN?",
"AT+CCID",
"AT+CGDCONT?",
"AT+CGPADDR",
"AT+CFUN?",
"AT+CLCC",
};
constexpr int kNumAtPresets =
static_cast<int>(sizeof(kAtPresets) / sizeof(kAtPresets[0]));
enum class AtTextMode {
Upper = 0, // ABC
Lower = 1, // abc
Num = 2, // 123
Sym = 3, // Sym
};
const char* kAtTapMap[10] = {
" 0+=",
"+?=,1\"':;/-_",
"ABC2",
"DEF3",
"GHI4",
"JKL5",
"MNO6",
"PQRS7",
"TUV8",
"WXYZ9"
};
void parseKvLines(const std::string& text,
std::unordered_map<std::string, std::string>& out,
@@ -231,6 +258,11 @@ ModemStatusData queryModemAndSimStatus() {
if (d.imei.empty()) {
d.imei = cleanMmVal(mmKv["modem.generic.equipment-identifier"]);
}
DeviceImeis devImeis = queryDeviceImeis();
if (!devImeis.imei1.empty()) d.imei = devImeis.imei1;
if (!devImeis.imei2.empty()) d.imei2 = devImeis.imei2;
if (!devImeis.imeiSv.empty()) d.imeiSv = devImeis.imeiSv;
if (d.imei2.empty()) d.imei2 = d.imei;
d.ownNumber = cleanMmVal(mmKv["modem.generic.own-numbers.value[1]"]);
d.simDbusPath = cleanMmVal(mmKv["modem.generic.sim"]);
d.currentModes = cleanMmVal(mmKv["modem.generic.current-modes"]);
@@ -452,6 +484,18 @@ int runModemApp(int argc, char** argv) {
std::string ussdResponse;
bool ussdWaitingReply = false;
// AT Console state
int atFocusRow = 0; // 0 = Input, 1 = Preset/Port, 2 = Output scroll
int atPresetIdx = 0;
int atPortIdx = 0; // 0 = auto, 1 = wwan0at0, 2 = wwan0at1
std::string atInput = "AT";
std::string atOutput = "Enter an AT command and press Send.\nUse Preset row (< >) for common commands.";
int atScrollLine = 0;
AtTextMode atTextMode = AtTextMode::Upper;
int atLastTapKey = -1;
int atLastTapIdx = 0;
int64_t atLastTapMs = 0;
std::string statusBanner;
int64_t statusBannerUntilMs = 0;
@@ -517,6 +561,8 @@ int runModemApp(int argc, char** argv) {
activeTab = TabId::ModemLog;
book.load();
logRows = loadRecentModemLog(book);
} else if (a == "at" || a == "atcmd") {
activeTab = TabId::AtConsole;
} else if (a == "ussd") {
activeTab = TabId::RadioInfo;
modal = ModalMode::UssdDialog;
@@ -604,9 +650,9 @@ int runModemApp(int argc, char** argv) {
XSetForeground(dpy, gc, pt.spec.border);
XDrawLine(dpy, pix, gc, 0, 29, kWinWidth, 29);
const char* tabLabels[3] = {"1:SIMs", "2:Radio", "3:Log"};
int tabW = kWinWidth / 3;
for (int t = 0; t < 3; ++t) {
const char* tabLabels[4] = {"1:SIMs", "2:Radio", "3:AT", "4:Log"};
int tabW = kWinWidth / 4;
for (int t = 0; t < 4; ++t) {
bool isSel = (static_cast<int>(activeTab) == t);
if (isSel) {
XSetForeground(dpy, gc, pt.spec.selBg);
@@ -742,12 +788,11 @@ int runModemApp(int argc, char** argv) {
(status.primaryGwSlot > 0 ? std::to_string(status.primaryGwSlot) : "--");
drawPango(layoutSmall, &colLight, 10, contentY + 42, 18, sigLine, 220);
std::string imeiLine = "IMEI: " + (status.imei.empty() ? "--" : status.imei);
drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imeiLine, 220);
std::string imei1Line = "IMEI 1: " + (status.imei.empty() ? "--" : status.imei);
drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imei1Line, 220);
std::string iccidLine = "ICCID: " +
(status.simIccid.empty() ? "--" : status.simIccid);
drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, iccidLine, 220);
std::string imei2Line = "IMEI 2: " + (status.imei2.empty() ? "--" : status.imei2);
drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, imei2Line, 220);
// 5 Interactive Radio Control Rows
std::string modeLabel = kRadioModes[status.modePresetIdx].label;
@@ -772,6 +817,75 @@ int runModemApp(int argc, char** argv) {
}
drawSoftkeyBar("Refresh", "Apply", "Close");
} else if (activeTab == TabId::AtConsole) {
const char* modeBadge = "[ABC]";
if (atTextMode == AtTextMode::Lower) modeBadge = "[abc]";
else if (atTextMode == AtTextMode::Num) modeBadge = "[123]";
else if (atTextMode == AtTextMode::Sym) modeBadge = "[Sym]";
const char* portNames[3] = {"auto", "wwan0at0", "wwan0at1"};
const char* portLabels[3] = {"Auto", "SIM1 (at0)", "SIM2 (at1)"};
(void)portNames;
// Row 0: AT Command Input Box (Y=contentY..contentY+46)
drawPango(layoutSmall, &colYellow, 8, contentY, 18,
std::string(kIconTerm) + " AT Command (# mode, * +?=):");
int mbW = pangoTextWidth(layoutSmall, modeBadge);
drawPango(layoutSmall, &colGreen, kWinWidth - mbW - 8, contentY, 18, modeBadge);
bool focus0 = (atFocusRow == 0);
XSetForeground(dpy, gc, focus0 ? pt.spec.bgInputFocus : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 12, 26);
XSetForeground(dpy, gc, focus0 ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 13, 25);
drawPango(layoutMain, &colWhite, 12, contentY + 19, 26,
atInput + (focus0 ? "_" : ""), kWinWidth - 24);
// Row 1: Preset & AT Port Selector (Y=contentY+49..contentY+73)
bool focus1 = (atFocusRow == 1);
XSetForeground(dpy, gc, focus1 ? pt.spec.selBg : pt.spec.bgAltOdd);
XFillRectangle(dpy, pix, gc, 6, contentY + 48, kWinWidth - 12, 24);
std::string presetRow = "< Preset: " + std::string(kAtPresets[atPresetIdx]) +
" | *Port: " + portLabels[atPortIdx] + " >";
drawPango(layoutSmall, focus1 ? &colWhite : &colLight,
10, contentY + 48, 24, presetRow, kWinWidth - 20);
// Row 2: AT Response Output Area (Y=contentY+75..contentY+contentH-2)
bool focus2 = (atFocusRow == 2);
int outY = contentY + 75;
int outH = contentH - 77;
XSetForeground(dpy, gc, pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 6, outY, kWinWidth - 12, outH);
XSetForeground(dpy, gc, focus2 ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 6, outY, kWinWidth - 13, outH - 1);
pango_layout_set_width(layoutSmall, (kWinWidth - 24) * PANGO_SCALE);
pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR);
pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE);
pango_layout_set_text(layoutSmall, atOutput.c_str(),
static_cast<int>(atOutput.size()));
int lineCount = pango_layout_get_line_count(layoutSmall);
int maxVisLines = std::max(1, (outH - 8) / 17);
if (atScrollLine > std::max(0, lineCount - maxVisLines)) {
atScrollLine = std::max(0, lineCount - maxVisLines);
}
int curY = outY + 4;
for (int li = atScrollLine;
li < lineCount && (li - atScrollLine) < maxVisLines; ++li) {
PangoLayoutLine* pl = pango_layout_get_line_readonly(layoutSmall, li);
if (pl) {
PangoRectangle ink{}, logical{};
pango_layout_line_get_pixel_extents(pl, &ink, &logical);
pango_xft_render_layout_line(
xftDraw, &colLight, pl, 12 * PANGO_SCALE,
(curY - logical.y) * PANGO_SCALE);
curY += std::max(16, logical.height + 1);
}
}
pango_layout_set_width(layoutSmall, -1);
drawSoftkeyBar("Send", "Send",
(atFocusRow == 0 && !atInput.empty()) ? "Clear" : "Close");
} else if (activeTab == TabId::ModemLog) {
if (logRows.empty()) {
drawPango(layoutMain, &colMuted, 16, contentY + 60, 24,
@@ -1144,16 +1258,17 @@ int runModemApp(int argc, char** argv) {
}
} else if (modal == ModalMode::UssdDialog) {
if (ussdInput.empty()) return;
ussdResponse = "Sending USSD request...";
if (ussdInput == "*#06#") {
DeviceImeis im = queryDeviceImeis();
ussdResponse = "IMEI 1: " + (im.imei1.empty() ? "--" : im.imei1) +
"\nIMEI 2: " + (im.imei2.empty() ? "--" : im.imei2) +
"\nIMEI SV: " + (im.imeiSv.empty() ? "--" : im.imeiSv);
ussdWaitingReply = false;
return;
}
ussdResponse = "Sending USSD " + ussdInput + "...";
render();
std::string cmd = ussdWaitingReply
? ("mmcli -m any --3gpp-ussd-respond=" + shellQuote(ussdInput) + " 2>&1")
: ("mmcli -m any --3gpp-ussd-initiate=" + shellQuote(ussdInput) + " 2>&1");
int rc = 0;
std::string out = trimStr(runCaptureCmd(cmd, &rc));
ussdResponse = out.empty() ? "(No response)" : out;
std::string stOut = runCaptureCmd("mmcli -m any --3gpp-ussd-status -K 2>/dev/null");
ussdWaitingReply = (stOut.find("user-response") != std::string::npos);
ussdResponse = sendModemUssd(ussdInput, ussdWaitingReply, &ussdWaitingReply);
if (ussdWaitingReply) {
ussdInput.clear();
}
@@ -1212,6 +1327,30 @@ int runModemApp(int argc, char** argv) {
}
};
auto executeAtCommand = [&]() {
std::string cmd = trimStr(atInput);
if (cmd.empty()) {
setBanner("AT command cannot be empty");
return;
}
const char* portArg = (atPortIdx == 1)
? "/dev/wwan0at0"
: (atPortIdx == 2 ? "/dev/wwan0at1" : "auto");
atOutput = "Sending: " + cmd + " ...";
atScrollLine = 0;
render();
std::string out = trimStr(runCaptureCmd(
"doas -n /usr/libexec/msm-modem-uim-selection at " +
shellQuote(portArg) + " " + shellQuote(cmd) + " 2>&1"));
if (out.empty()) {
atOutput = "> " + cmd + "\n(No response)";
} else {
atOutput = "> " + cmd + "\n" + out;
}
atScrollLine = 0;
setBanner("AT response received");
};
auto handleLeftSoftkey = [&]() {
if (modal == ModalMode::SimActions) {
int act = actionSel;
@@ -1256,6 +1395,8 @@ int runModemApp(int argc, char** argv) {
} else if (activeTab == TabId::RadioInfo) {
refreshData(false);
setBanner("Modem status refreshed");
} else if (activeTab == TabId::AtConsole) {
executeAtCommand();
} else if (activeTab == TabId::ModemLog) {
refreshData(true);
setBanner("Modem log refreshed");
@@ -1312,6 +1453,12 @@ int runModemApp(int argc, char** argv) {
refreshData(false);
setBanner("Refreshed");
}
} else if (activeTab == TabId::AtConsole) {
if (atFocusRow == 1) {
atPortIdx = (atPortIdx + 1) % 3;
} else {
executeAtCommand();
}
} else if (activeTab == TabId::ModemLog) {
if (!logRows.empty() && logRowSel >= 0 &&
logRowSel < static_cast<int>(logRows.size())) {
@@ -1474,8 +1621,84 @@ int runModemApp(int argc, char** argv) {
continue;
}
// Main View Navigation
// AT Console text input when atFocusRow == 0
char dch = mapDialKey(sym, kc, ev.xkey.state);
if (activeTab == TabId::AtConsole && atFocusRow == 0) {
if (sym == XK_BackSpace || sym == XK_Delete) {
if (!atInput.empty()) {
atInput.pop_back();
atLastTapKey = -1;
render();
}
continue;
}
if (dch == '#') {
atTextMode = static_cast<AtTextMode>(
(static_cast<int>(atTextMode) + 1) % 4);
atLastTapKey = -1;
render();
continue;
}
if (dch == '*') {
const char* syms = "+?=,1\"':;/-_*#";
int len = static_cast<int>(std::strlen(syms));
if (atLastTapKey == 10 && (now - atLastTapMs) < 850 &&
!atInput.empty()) {
atLastTapIdx = (atLastTapIdx + 1) % len;
atInput.back() = syms[atLastTapIdx];
} else if (atInput.size() < 64) {
atLastTapKey = 10;
atLastTapIdx = 0;
atInput.push_back(syms[0]);
}
atLastTapMs = now;
render();
continue;
}
if (dch >= '0' && dch <= '9') {
int digit = dch - '0';
if (atTextMode == AtTextMode::Num) {
if (atInput.size() < 64) {
atInput.push_back(dch);
atLastTapKey = -1;
}
} else if (atTextMode == AtTextMode::Sym) {
const char* symTable = "+?=,.\"':;/-_";
char sc = symTable[digit % 10];
if (atInput.size() < 64) {
atInput.push_back(sc);
atLastTapKey = -1;
}
} else {
const char* seq = kAtTapMap[digit];
int len = static_cast<int>(std::strlen(seq));
if (atLastTapKey == digit && (now - atLastTapMs) < 850 &&
!atInput.empty()) {
atLastTapIdx = (atLastTapIdx + 1) % len;
char chOut = seq[atLastTapIdx];
if (atTextMode == AtTextMode::Lower) {
chOut = static_cast<char>(
std::tolower(static_cast<unsigned char>(chOut)));
}
atInput.back() = chOut;
} else if (atInput.size() < 64) {
atLastTapKey = digit;
atLastTapIdx = 0;
char chOut = seq[0];
if (atTextMode == AtTextMode::Lower) {
chOut = static_cast<char>(
std::tolower(static_cast<unsigned char>(chOut)));
}
atInput.push_back(chOut);
}
atLastTapMs = now;
}
render();
continue;
}
}
// Main View Navigation (4 tabs: 1:SIMs, 2:Radio, 3:AT, 4:Log)
if (dch == '1') {
activeTab = TabId::SimCards;
render();
@@ -1485,25 +1708,39 @@ int runModemApp(int argc, char** argv) {
render();
continue;
} else if (dch == '3') {
activeTab = TabId::AtConsole;
render();
continue;
} else if (dch == '4') {
activeTab = TabId::ModemLog;
refreshData(true);
render();
continue;
} else if (activeTab == TabId::AtConsole && dch == '*') {
atPortIdx = (atPortIdx + 1) % 3;
render();
continue;
}
if (sym == XK_Left || sym == XK_KP_Left) {
if (activeTab == TabId::RadioInfo && radioRowSel == 0) {
applyRadioModePreset(status.modePresetIdx - 1);
} else if (activeTab == TabId::AtConsole && atFocusRow == 1) {
atPresetIdx = (atPresetIdx + kNumAtPresets - 1) % kNumAtPresets;
atInput = kAtPresets[atPresetIdx];
} else {
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 2) % 3);
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 3) % 4);
if (activeTab == TabId::ModemLog) refreshData(true);
}
render();
} else if (sym == XK_Right || sym == XK_KP_Right) {
if (activeTab == TabId::RadioInfo && radioRowSel == 0) {
applyRadioModePreset(status.modePresetIdx + 1);
} else if (activeTab == TabId::AtConsole && atFocusRow == 1) {
atPresetIdx = (atPresetIdx + 1) % kNumAtPresets;
atInput = kAtPresets[atPresetIdx];
} else {
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 1) % 3);
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 1) % 4);
if (activeTab == TabId::ModemLog) refreshData(true);
}
render();
@@ -1512,6 +1749,12 @@ int runModemApp(int argc, char** argv) {
simRowSel = (simRowSel + 5) % 6;
} else if (activeTab == TabId::RadioInfo) {
radioRowSel = (radioRowSel + 4) % 5;
} else if (activeTab == TabId::AtConsole) {
if (atFocusRow == 2 && atScrollLine > 0) {
--atScrollLine;
} else {
atFocusRow = (atFocusRow + 2) % 3;
}
} else if (activeTab == TabId::ModemLog && !logRows.empty()) {
logRowSel = std::max(0, logRowSel - 1);
}
@@ -1521,11 +1764,19 @@ int runModemApp(int argc, char** argv) {
simRowSel = (simRowSel + 1) % 6;
} else if (activeTab == TabId::RadioInfo) {
radioRowSel = (radioRowSel + 1) % 5;
} else if (activeTab == TabId::AtConsole) {
if (atFocusRow == 2) {
++atScrollLine;
} else {
atFocusRow = (atFocusRow + 1) % 3;
}
} else if (activeTab == TabId::ModemLog && !logRows.empty()) {
logRowSel = std::min(static_cast<int>(logRows.size()) - 1, logRowSel + 1);
}
render();
} else if (sym == XK_Return || sym == XK_KP_Enter) {
} else if (sym == XK_Return || sym == XK_KP_Enter ||
sym == XF86XK_Send || sym == XF86XK_Phone ||
sym == XF86XK_PickupPhone) {
handleEnterKey();
render();
}
@@ -1565,7 +1816,8 @@ int runModemApp(int argc, char** argv) {
}
}
// Periodic 5-second status refresh while visible on SIM/Radio tabs
if (modal == ModalMode::None && activeTab != TabId::ModemLog) {
if (modal == ModalMode::None && activeTab != TabId::ModemLog &&
activeTab != TabId::AtConsole) {
int64_t rRem = (lastRefreshMs + 5000) - now;
if (rRem <= 0) {
refreshData(false);