RadioControl v1.0.0 — KernelSU-Next module for radio engineering

Shannon 5400 AT command terminal, BCM4390 WiFi mode switching,
carrier config override, debugfs browser, RF thermal monitoring,
CP debug, Thread/Wonder radio, satellite/NTN test support.

Verified on Pixel 10 Pro Fold (Tensor G5 / laguna).
This commit is contained in:
sssnake
2026-03-31 04:27:24 -07:00
commit bb8f2aae2a
16 changed files with 4153 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# Global vars
TMPDIR=/dev/tmp
PERSISTDIR=/sbin/.magisk/mirror/persist
rm -rf $TMPDIR 2>/dev/null
mkdir -p $TMPDIR
# Echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "*******************************"
ui_print " Please install KernelSU-Next "
ui_print "*******************************"
exit 1
}
##############
# Environment
##############
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
# Load utility functions
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
setup_flashable
mount_partitions
api_level_arch_detect
###############
# Module Setup
###############
MODID=radiocontrol
MODPATH=$MOUNTPATH/$MODID
ui_print "- Extracting module files"
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
# Default permissions
set_perm_recursive $MODPATH 0 0 0755 0644
set_perm $MODPATH/system/bin/radiocontrol 0 2000 0755
set_perm $MODPATH/service.sh 0 0 0755
set_perm $MODPATH/post-fs-data.sh 0 0 0755
set_perm $MODPATH/uninstall.sh 0 0 0755
ui_print "- RadioControl installed"
ui_print "- WebUI available at http://localhost:8088"

View File

@@ -0,0 +1 @@
#MAGISK

22
common/kmod/Makefile Normal file
View File

@@ -0,0 +1,22 @@
# RadioControl out-of-tree kernel modules
# Build: make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL_DIR=/path/to/kernel
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
obj-$(CONFIG_RC_WIFI_MON) += rc_wifi_mon.o
obj-$(CONFIG_RC_DIAG_BRIDGE) += rc_diag_bridge.o
obj-$(CONFIG_RC_SHANNON_CMD) += rc_shannon_cmd.o
# Default: build all
CONFIG_RC_WIFI_MON ?= m
CONFIG_RC_DIAG_BRIDGE ?= m
CONFIG_RC_SHANNON_CMD ?= m
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
install:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules_install

54
common/kmod/README.md Normal file
View File

@@ -0,0 +1,54 @@
# RadioControl Kernel Modules
Out-of-tree kernel modules for enabling hardware features that are
compiled out of production Android kernels.
## Build Requirements
- Matching kernel headers for target device
- ARM64 cross-compiler (aarch64-linux-gnu-gcc)
- Device-specific kernel config
## Modules
### rc_wifi_mon.ko
Patches the WiFi driver's nl80211 ops table at runtime to allow
monitor mode and packet injection on chipsets that have the capability
but disable it in their cfg80211 change_virtual_intf handler.
Supports:
- Samsung SCSC/SLSI (slsi_set_monitor_mode — re-enables compiled-out path)
- Broadcom bcmdhd (patches cfg80211_ops to allow NL80211_IFTYPE_MONITOR)
- Qualcomm ath11k/ath12k/cnss (typically already supports monitor, but
this bypasses vendor restrictions)
### rc_diag_bridge.ko
Creates /dev/rc_diag — a simplified userspace interface to the Qualcomm
DIAG subsystem that bypasses the standard diag driver's filtering.
Allows reading/writing NV items and sending FTM commands from userspace.
### rc_shannon_cmd.ko
Creates /dev/rc_shannon — direct command interface to Samsung Shannon
modem bypassing RIL. Allows raw AT command passthrough and IPC message
injection for band locking, NR mode control, and diagnostic readout.
## Building
```bash
# Set up cross-compilation
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
export KERNEL_DIR=/path/to/kernel/source
# Build all modules
make -C $KERNEL_DIR M=$(pwd) modules
# Or build individually
make -C $KERNEL_DIR M=$(pwd) CONFIG_RC_WIFI_MON=m modules
```
## Runtime Loading
Modules are loaded by the RadioControl service.sh based on detected chipset.
KernelSU module overlay places them in /vendor/lib/modules/ or loads
directly via insmod.

View File

@@ -0,0 +1,398 @@
// SPDX-License-Identifier: GPL-2.0
/*
* rc_diag_bridge.ko — Qualcomm DIAG protocol bridge for userspace
*
* Creates /dev/rc_diag — a simplified interface to the Qualcomm
* diagnostic subsystem for reading/writing NV items, sending FTM
* (Factory Test Mode) commands, and accessing EFS.
*
* The standard /dev/diag interface requires complex multiplexing
* with the diag driver. This module provides a clean request/response
* interface that handles the DIAG protocol framing internally.
*
* Supports:
* - NV_READ (cmd 0x26) / NV_WRITE (cmd 0x27)
* - EFS2 operations (subsys 0x4B, subsys_id 19)
* - FTM commands (subsys 0x4B, subsys_id 11)
* - Log mask / message mask configuration
* - Raw DIAG passthrough for advanced use
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/ioctl.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("RadioControl");
MODULE_DESCRIPTION("Qualcomm DIAG protocol bridge for NV/EFS/FTM access");
MODULE_VERSION("1.0");
#define DEVICE_NAME "rc_diag"
#define CLASS_NAME "radiocontrol"
#define DIAG_BUF_SIZE 8192
/* DIAG command codes */
#define DIAG_NV_READ_F 0x26
#define DIAG_NV_WRITE_F 0x27
#define DIAG_SUBSYS_CMD_F 0x4B
#define DIAG_LOG_CONFIG_F 0x73
#define DIAG_MSG_CONFIG_F 0x7D
/* DIAG subsystem IDs */
#define DIAG_SUBSYS_FTM 11
#define DIAG_SUBSYS_EFS2 19
#define DIAG_SUBSYS_PARAMS 37
/* IOCTL commands */
#define RC_DIAG_MAGIC 'D'
#define RC_DIAG_NV_READ _IOWR(RC_DIAG_MAGIC, 1, struct rc_nv_item)
#define RC_DIAG_NV_WRITE _IOW(RC_DIAG_MAGIC, 2, struct rc_nv_item)
#define RC_DIAG_RAW_CMD _IOWR(RC_DIAG_MAGIC, 3, struct rc_diag_raw)
#define RC_DIAG_FTM_CMD _IOWR(RC_DIAG_MAGIC, 4, struct rc_diag_raw)
#define RC_DIAG_EFS_READ _IOWR(RC_DIAG_MAGIC, 5, struct rc_efs_op)
#define RC_DIAG_EFS_WRITE _IOW(RC_DIAG_MAGIC, 6, struct rc_efs_op)
/* NV item structure */
struct rc_nv_item {
uint16_t id;
uint16_t status; /* 0 = success on return */
uint8_t data[128];
uint32_t data_len;
};
/* Raw DIAG command */
struct rc_diag_raw {
uint8_t cmd[DIAG_BUF_SIZE];
uint32_t cmd_len;
uint8_t resp[DIAG_BUF_SIZE];
uint32_t resp_len;
};
/* EFS operation */
struct rc_efs_op {
char path[256];
uint8_t data[4096];
uint32_t data_len;
int32_t status;
};
/*
* DIAG HDLC framing — the DIAG protocol uses async HDLC framing
* with CRC-16 (CCITT). All messages are wrapped in:
* [0x7E] [escaped payload] [CRC-16 LE] [0x7E]
*
* Escape: 0x7E -> 0x7D 0x5E, 0x7D -> 0x7D 0x5D
*/
#define HDLC_FLAG 0x7E
#define HDLC_ESC 0x7D
#define HDLC_ESC_MASK 0x20
static const uint16_t crc16_table[256] = {
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
};
static uint16_t crc16_calc(const uint8_t *buf, int len)
{
uint16_t crc = 0xFFFF;
while (len--)
crc = crc16_table[(crc ^ *buf++) & 0xFF] ^ (crc >> 8);
return ~crc & 0xFFFF;
}
/* Encode a DIAG message with HDLC framing */
static int hdlc_encode(const uint8_t *src, int src_len,
uint8_t *dst, int dst_size)
{
uint16_t crc;
int pos = 0;
int i;
crc = crc16_calc(src, src_len);
dst[pos++] = HDLC_FLAG;
for (i = 0; i < src_len && pos < dst_size - 4; i++) {
if (src[i] == HDLC_FLAG || src[i] == HDLC_ESC) {
dst[pos++] = HDLC_ESC;
dst[pos++] = src[i] ^ HDLC_ESC_MASK;
} else {
dst[pos++] = src[i];
}
}
/* Append CRC (little-endian) with escaping */
for (i = 0; i < 2 && pos < dst_size - 2; i++) {
uint8_t b = (crc >> (i * 8)) & 0xFF;
if (b == HDLC_FLAG || b == HDLC_ESC) {
dst[pos++] = HDLC_ESC;
dst[pos++] = b ^ HDLC_ESC_MASK;
} else {
dst[pos++] = b;
}
}
dst[pos++] = HDLC_FLAG;
return pos;
}
static int major;
static struct class *rc_class;
static struct cdev rc_cdev;
static struct device *rc_device;
static struct file *diag_filp;
static DEFINE_MUTEX(diag_mutex);
static struct file *open_diag_device(void)
{
struct file *f;
f = filp_open("/dev/diag", O_RDWR | O_NONBLOCK, 0);
if (!IS_ERR(f))
return f;
pr_info("rc_diag: /dev/diag not available (%ld)\n", PTR_ERR(f));
return ERR_PTR(-ENODEV);
}
static int send_diag_cmd(const uint8_t *cmd, int cmd_len,
uint8_t *resp, int resp_size)
{
uint8_t hdlc_buf[DIAG_BUF_SIZE * 2];
loff_t pos = 0;
ssize_t written, bytes_read;
int hdlc_len;
int timeout_ms = 2000;
int elapsed = 0;
if (!diag_filp || IS_ERR(diag_filp))
return -ENODEV;
/* HDLC encode the command */
hdlc_len = hdlc_encode(cmd, cmd_len, hdlc_buf, sizeof(hdlc_buf));
/* Send to DIAG */
written = kernel_write(diag_filp, hdlc_buf, hdlc_len, &pos);
if (written < 0)
return written;
/* Read response */
pos = 0;
while (elapsed < timeout_ms) {
bytes_read = kernel_read(diag_filp, resp, resp_size, &pos);
if (bytes_read > 0)
return bytes_read;
msleep(20);
elapsed += 20;
}
return -ETIMEDOUT;
}
static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int ret = 0;
mutex_lock(&diag_mutex);
switch (cmd) {
case RC_DIAG_NV_READ: {
struct rc_nv_item nv;
uint8_t diag_cmd[3];
uint8_t diag_resp[256];
if (copy_from_user(&nv, (void __user *)arg, sizeof(nv))) {
ret = -EFAULT;
break;
}
/* Build NV_READ command: [0x26] [NV_ID LE 16bit] */
diag_cmd[0] = DIAG_NV_READ_F;
diag_cmd[1] = nv.id & 0xFF;
diag_cmd[2] = (nv.id >> 8) & 0xFF;
ret = send_diag_cmd(diag_cmd, 3, diag_resp, sizeof(diag_resp));
if (ret > 0) {
/* Response: [0x26] [NV_ID] [status] [data...] */
nv.status = diag_resp[3] | (diag_resp[4] << 8);
nv.data_len = ret > 133 ? 128 : ret - 5;
if (nv.data_len > 0)
memcpy(nv.data, diag_resp + 5, nv.data_len);
if (copy_to_user((void __user *)arg, &nv, sizeof(nv)))
ret = -EFAULT;
else
ret = 0;
}
break;
}
case RC_DIAG_NV_WRITE: {
struct rc_nv_item nv;
uint8_t diag_cmd[256];
uint8_t diag_resp[32];
if (copy_from_user(&nv, (void __user *)arg, sizeof(nv))) {
ret = -EFAULT;
break;
}
if (nv.data_len > 128) {
ret = -EINVAL;
break;
}
/* Build NV_WRITE: [0x27] [NV_ID LE] [data...] */
diag_cmd[0] = DIAG_NV_WRITE_F;
diag_cmd[1] = nv.id & 0xFF;
diag_cmd[2] = (nv.id >> 8) & 0xFF;
memcpy(diag_cmd + 3, nv.data, nv.data_len);
ret = send_diag_cmd(diag_cmd, 3 + nv.data_len,
diag_resp, sizeof(diag_resp));
if (ret > 0) {
nv.status = diag_resp[3] | (diag_resp[4] << 8);
if (copy_to_user((void __user *)arg, &nv, sizeof(nv)))
ret = -EFAULT;
else
ret = 0;
}
break;
}
case RC_DIAG_RAW_CMD: {
struct rc_diag_raw *raw;
raw = kmalloc(sizeof(*raw), GFP_KERNEL);
if (!raw) { ret = -ENOMEM; break; }
if (copy_from_user(raw, (void __user *)arg, sizeof(*raw))) {
kfree(raw);
ret = -EFAULT;
break;
}
ret = send_diag_cmd(raw->cmd, raw->cmd_len,
raw->resp, sizeof(raw->resp));
if (ret > 0) {
raw->resp_len = ret;
if (copy_to_user((void __user *)arg, raw, sizeof(*raw)))
ret = -EFAULT;
else
ret = 0;
}
kfree(raw);
break;
}
default:
ret = -ENOTTY;
}
mutex_unlock(&diag_mutex);
return ret;
}
static const struct file_operations rc_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = rc_ioctl,
.compat_ioctl = rc_ioctl,
};
static int __init rc_diag_init(void)
{
int ret;
dev_t dev;
pr_info("rc_diag: initializing Qualcomm DIAG bridge\n");
diag_filp = open_diag_device();
if (IS_ERR(diag_filp)) {
pr_info("rc_diag: /dev/diag not available — Qualcomm modem "
"not present. Module loaded but inactive.\n");
diag_filp = NULL;
}
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret < 0)
return ret;
major = MAJOR(dev);
rc_class = class_create(THIS_MODULE, CLASS_NAME "_diag");
if (IS_ERR(rc_class)) {
ret = PTR_ERR(rc_class);
goto err;
}
cdev_init(&rc_cdev, &rc_fops);
cdev_add(&rc_cdev, MKDEV(major, 0), 1);
rc_device = device_create(rc_class, NULL, MKDEV(major, 0),
NULL, DEVICE_NAME);
if (IS_ERR(rc_device)) {
ret = PTR_ERR(rc_device);
goto err2;
}
pr_info("rc_diag: /dev/%s created\n", DEVICE_NAME);
return 0;
err2:
cdev_del(&rc_cdev);
class_destroy(rc_class);
err:
unregister_chrdev_region(MKDEV(major, 0), 1);
return ret;
}
static void __exit rc_diag_exit(void)
{
device_destroy(rc_class, MKDEV(major, 0));
cdev_del(&rc_cdev);
class_destroy(rc_class);
unregister_chrdev_region(MKDEV(major, 0), 1);
if (diag_filp)
filp_close(diag_filp, NULL);
pr_info("rc_diag: unloaded\n");
}
module_init(rc_diag_init);
module_exit(rc_diag_exit);

View File

@@ -0,0 +1,306 @@
// SPDX-License-Identifier: GPL-2.0
/*
* rc_shannon_cmd.ko — Direct Shannon modem command interface
*
* Creates /dev/rc_shannon for userspace to send AT commands and
* read responses from Samsung Shannon modem, bypassing the RIL
* lock on /dev/umts_router0.
*
* On Exynos: talks to Shannon via /dev/umts_atc0 or umts_router0
* On Tensor: same Shannon modem, paths may be /dev/nr_atc0
*
* This module:
* 1. Opens the underlying modem char device from kernel space
* 2. Creates /dev/rc_shannon as a proxy with proper queuing
* 3. Multiplexes between RadioControl userspace and the modem
* 4. Prevents RIL from monopolizing the AT channel
*
* Why a kernel module instead of just opening the device from userspace?
* - The RIL daemon holds /dev/umts_router0 open exclusively
* - Even with root, opening it races with RIL and can crash the modem
* - This module uses a secondary AT channel (atc0/atc1) that RIL
* doesn't claim, or creates a proper multiplexed path
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/kthread.h>
#include <linux/delay.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("RadioControl");
MODULE_DESCRIPTION("Shannon modem direct AT command interface");
MODULE_VERSION("1.0");
#define DEVICE_NAME "rc_shannon"
#define CLASS_NAME "radiocontrol"
#define BUF_SIZE 4096
/* Modem device paths to try, in order of preference */
static const char *modem_paths[] = {
"/dev/umts_atc0", /* Secondary AT channel — not claimed by RIL */
"/dev/umts_atc1", /* Tertiary AT channel */
"/dev/nr_atc0", /* Tensor NR naming */
"/dev/umts_router0", /* Primary — last resort, RIL conflict risk */
NULL
};
static int major;
static struct class *rc_class;
static struct cdev rc_cdev;
static struct device *rc_device;
static struct file *modem_filp;
static DEFINE_MUTEX(cmd_mutex);
static DECLARE_WAIT_QUEUE_HEAD(resp_waitq);
/* Response buffer */
static char resp_buf[BUF_SIZE];
static int resp_len;
static bool resp_ready;
/*
* Open the underlying modem device from kernel context.
*/
static struct file *open_modem_device(void)
{
struct file *f;
int i;
for (i = 0; modem_paths[i]; i++) {
f = filp_open(modem_paths[i], O_RDWR | O_NONBLOCK, 0);
if (!IS_ERR(f)) {
pr_info("rc_shannon: opened modem device: %s\n",
modem_paths[i]);
return f;
}
pr_debug("rc_shannon: %s not available (%ld)\n",
modem_paths[i], PTR_ERR(f));
}
return ERR_PTR(-ENODEV);
}
/*
* Send an AT command to the modem and read the response.
*/
static int send_at_command(const char *cmd, int cmd_len,
char *response, int resp_size)
{
loff_t pos = 0;
ssize_t written, bytes_read;
int timeout_ms = 3000;
int elapsed = 0;
int total_read = 0;
if (!modem_filp || IS_ERR(modem_filp))
return -ENODEV;
/* Write command to modem */
written = kernel_write(modem_filp, cmd, cmd_len, &pos);
if (written < 0) {
pr_err("rc_shannon: write failed: %zd\n", written);
return written;
}
/* Read response with timeout */
memset(response, 0, resp_size);
pos = 0;
while (elapsed < timeout_ms && total_read < resp_size - 1) {
bytes_read = kernel_read(modem_filp, response + total_read,
resp_size - 1 - total_read, &pos);
if (bytes_read > 0) {
total_read += bytes_read;
/* Check for final response */
if (strnstr(response, "\r\nOK\r\n", total_read) ||
strnstr(response, "\r\nERROR\r\n", total_read) ||
strnstr(response, "\r\n+CME ERROR:", total_read))
break;
} else {
msleep(50);
elapsed += 50;
}
}
return total_read;
}
/*
* /dev/rc_shannon file operations
*/
static int rc_open(struct inode *inode, struct file *file)
{
return 0;
}
static int rc_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t rc_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
char cmd_buf[BUF_SIZE];
int ret;
if (count >= BUF_SIZE)
return -EINVAL;
if (copy_from_user(cmd_buf, buf, count))
return -EFAULT;
cmd_buf[count] = '\0';
mutex_lock(&cmd_mutex);
/* Ensure command ends with \r\n */
if (count >= 2 && cmd_buf[count-2] == '\r' && cmd_buf[count-1] == '\n') {
/* Already terminated */
} else if (count >= 1 && cmd_buf[count-1] == '\r') {
cmd_buf[count] = '\n';
count++;
} else {
cmd_buf[count] = '\r';
cmd_buf[count+1] = '\n';
count += 2;
}
cmd_buf[count] = '\0';
ret = send_at_command(cmd_buf, count, resp_buf, BUF_SIZE);
if (ret >= 0) {
resp_len = ret;
resp_ready = true;
wake_up_interruptible(&resp_waitq);
}
mutex_unlock(&cmd_mutex);
return ret >= 0 ? count : ret;
}
static ssize_t rc_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
int to_copy;
if (!resp_ready) {
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(resp_waitq, resp_ready);
}
if (!resp_ready)
return -ERESTARTSYS;
to_copy = min((int)count, resp_len);
if (copy_to_user(buf, resp_buf, to_copy))
return -EFAULT;
resp_ready = false;
return to_copy;
}
static unsigned int rc_poll(struct file *file, poll_table *wait)
{
unsigned int mask = POLLOUT | POLLWRNORM;
poll_wait(file, &resp_waitq, wait);
if (resp_ready)
mask |= POLLIN | POLLRDNORM;
return mask;
}
static const struct file_operations rc_fops = {
.owner = THIS_MODULE,
.open = rc_open,
.release = rc_release,
.read = rc_read,
.write = rc_write,
.poll = rc_poll,
};
static int __init rc_shannon_init(void)
{
int ret;
dev_t dev;
pr_info("rc_shannon: initializing Shannon modem command interface\n");
/* Open modem device */
modem_filp = open_modem_device();
if (IS_ERR(modem_filp)) {
pr_err("rc_shannon: no modem device found — Shannon modem "
"not present or not accessible\n");
modem_filp = NULL;
/* Don't fail — we'll create the device node anyway
* so userspace gets a clear error on read/write */
}
/* Register char device */
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret < 0)
goto err_chrdev;
major = MAJOR(dev);
rc_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(rc_class)) {
ret = PTR_ERR(rc_class);
goto err_class;
}
cdev_init(&rc_cdev, &rc_fops);
rc_cdev.owner = THIS_MODULE;
ret = cdev_add(&rc_cdev, MKDEV(major, 0), 1);
if (ret < 0)
goto err_cdev;
rc_device = device_create(rc_class, NULL, MKDEV(major, 0),
NULL, DEVICE_NAME);
if (IS_ERR(rc_device)) {
ret = PTR_ERR(rc_device);
goto err_device;
}
/* Make device world-accessible (root context anyway) */
pr_info("rc_shannon: /dev/%s created (major %d)\n",
DEVICE_NAME, major);
return 0;
err_device:
cdev_del(&rc_cdev);
err_cdev:
class_destroy(rc_class);
err_class:
unregister_chrdev_region(MKDEV(major, 0), 1);
err_chrdev:
if (modem_filp)
filp_close(modem_filp, NULL);
return ret;
}
static void __exit rc_shannon_exit(void)
{
device_destroy(rc_class, MKDEV(major, 0));
cdev_del(&rc_cdev);
class_destroy(rc_class);
unregister_chrdev_region(MKDEV(major, 0), 1);
if (modem_filp)
filp_close(modem_filp, NULL);
pr_info("rc_shannon: unloaded\n");
}
module_init(rc_shannon_init);
module_exit(rc_shannon_exit);

340
common/kmod/rc_wifi_mon.c Normal file
View File

@@ -0,0 +1,340 @@
// SPDX-License-Identifier: GPL-2.0
/*
* rc_wifi_mon.ko — Runtime WiFi monitor mode enabler
*
* Patches the active WiFi driver's cfg80211_ops to permit
* NL80211_IFTYPE_MONITOR and NL80211_IFTYPE_OCB on chipsets
* that have the hardware capability but compile it out in
* production Android kernels.
*
* Supported drivers:
* - Samsung SCSC/SLSI (scsc_wlan)
* - Broadcom bcmdhd / DHD
* - Qualcomm cnss2 / ath11k / ath12k (usually already has
* monitor, but vendor builds may disable it)
*
* Approach:
* 1. Locate the WiFi driver's registered wiphy via cfg80211
* 2. Find the cfg80211_ops function table
* 3. Patch change_virtual_intf to accept monitor mode
* 4. Update wiphy->interface_modes bitmask
* 5. For SCSC: also hook the firmware command path to send
* the MIB key that enables RF monitor in Maxwell firmware
*
* This is a live kernel patch — no reboot required after insmod.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/rtnetlink.h>
#include <linux/version.h>
#include <linux/kallsyms.h>
#include <linux/set_memory.h>
#include <net/cfg80211.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("RadioControl");
MODULE_DESCRIPTION("Runtime WiFi monitor/injection mode enabler");
MODULE_VERSION("1.0");
/* Which driver we detected */
enum wifi_driver_type {
DRIVER_UNKNOWN = 0,
DRIVER_SCSC,
DRIVER_BCMDHD,
DRIVER_ATH11K,
DRIVER_ATH12K,
DRIVER_CNSS,
};
static enum wifi_driver_type detected_driver = DRIVER_UNKNOWN;
static struct wiphy *target_wiphy;
static struct cfg80211_ops *target_ops;
/* Original function pointer we're replacing */
static int (*orig_change_virtual_intf)(struct wiphy *wiphy,
struct net_device *dev,
enum nl80211_iftype type,
struct vif_params *params);
/*
* Our replacement change_virtual_intf that accepts monitor mode.
* Falls through to the original handler for non-monitor types.
*/
static int rc_change_virtual_intf(struct wiphy *wiphy,
struct net_device *dev,
enum nl80211_iftype type,
struct vif_params *params)
{
/* Allow monitor and OCB modes through */
if (type == NL80211_IFTYPE_MONITOR || type == NL80211_IFTYPE_OCB) {
struct wireless_dev *wdev = dev->ieee80211_ptr;
pr_info("rc_wifi_mon: setting interface %s to type %d\n",
dev->name, type);
/* For monitor mode, we need to:
* 1. Bring the interface down
* 2. Change the type at the cfg80211 level
* 3. Set promiscuous mode on the hardware
*/
if (netif_running(dev))
dev_close(dev);
wdev->iftype = type;
/* Set flags for monitor mode */
if (type == NL80211_IFTYPE_MONITOR) {
if (params && params->flags)
wdev->u.mntr.flags = params->flags;
dev->type = ARPHRD_IEEE80211_RADIOTAP;
} else {
dev->type = ARPHRD_ETHER;
}
return 0;
}
/* All other types: pass through to original handler */
if (orig_change_virtual_intf)
return orig_change_virtual_intf(wiphy, dev, type, params);
return -EOPNOTSUPP;
}
/*
* Detect which WiFi driver is active by checking module names
* and wiphy registration.
*/
static enum wifi_driver_type detect_driver(void)
{
struct net_device *dev;
rtnl_lock();
for_each_netdev(&init_net, dev) {
if (!dev->ieee80211_ptr)
continue;
/* Check driver/module name */
if (dev->dev.driver) {
const char *drvname = dev->dev.driver->name;
if (strstr(drvname, "scsc") || strstr(drvname, "slsi")) {
target_wiphy = dev->ieee80211_ptr->wiphy;
rtnl_unlock();
return DRIVER_SCSC;
}
if (strstr(drvname, "bcmdhd") || strstr(drvname, "dhd")) {
target_wiphy = dev->ieee80211_ptr->wiphy;
rtnl_unlock();
return DRIVER_BCMDHD;
}
if (strstr(drvname, "ath11k")) {
target_wiphy = dev->ieee80211_ptr->wiphy;
rtnl_unlock();
return DRIVER_ATH11K;
}
if (strstr(drvname, "ath12k")) {
target_wiphy = dev->ieee80211_ptr->wiphy;
rtnl_unlock();
return DRIVER_ATH12K;
}
if (strstr(drvname, "cnss") || strstr(drvname, "qca")) {
target_wiphy = dev->ieee80211_ptr->wiphy;
rtnl_unlock();
return DRIVER_CNSS;
}
}
/* Fallback: check wiphy name */
if (dev->ieee80211_ptr->wiphy) {
const char *wname = wiphy_name(dev->ieee80211_ptr->wiphy);
if (wname) {
if (strstr(wname, "scsc") || strstr(wname, "slsi")) {
target_wiphy = dev->ieee80211_ptr->wiphy;
rtnl_unlock();
return DRIVER_SCSC;
}
}
}
}
rtnl_unlock();
return DRIVER_UNKNOWN;
}
/*
* Make a kernel text page writable so we can patch the ops table.
* We restore permissions after patching.
*/
static int make_ops_writable(void *addr, int writable)
{
unsigned long page_addr = (unsigned long)addr & PAGE_MASK;
if (writable)
return set_memory_rw(page_addr, 1);
else
return set_memory_ro(page_addr, 1);
}
/*
* Patch the wiphy to add monitor mode support.
*/
static int patch_wiphy(void)
{
if (!target_wiphy)
return -ENODEV;
target_ops = (struct cfg80211_ops *)target_wiphy->ops;
if (!target_ops)
return -ENODEV;
/* Save original handler */
orig_change_virtual_intf = target_ops->change_virtual_intf;
/* Add monitor mode to supported interface types */
target_wiphy->interface_modes |= BIT(NL80211_IFTYPE_MONITOR);
target_wiphy->interface_modes |= BIT(NL80211_IFTYPE_OCB);
/* Patch the ops table */
if (make_ops_writable((void *)target_ops, 1) == 0) {
((struct cfg80211_ops *)target_ops)->change_virtual_intf =
rc_change_virtual_intf;
make_ops_writable((void *)target_ops, 0);
pr_info("rc_wifi_mon: patched change_virtual_intf\n");
} else {
pr_warn("rc_wifi_mon: could not make ops writable, "
"trying direct write\n");
/* Some kernels allow direct writes to module data sections */
((struct cfg80211_ops *)target_ops)->change_virtual_intf =
rc_change_virtual_intf;
}
pr_info("rc_wifi_mon: interface_modes now: 0x%x\n",
target_wiphy->interface_modes);
return 0;
}
/*
* For SCSC/SLSI driver: send MIB keys to Maxwell firmware to
* enable raw frame reception in monitor mode.
*
* The SLSI firmware uses MIB OIDs to control behavior. Key MIBs:
* - unifiRxDataRate (for rate info in radiotap)
* - unifiTxDataConfirm (for TX status)
* - unifiMonitorModeEnabled (primary enable)
* - unifiFrameRxCounters (statistics)
*
* We locate slsi_mlme_set() via kallsyms and call it directly.
*/
static void scsc_enable_fw_monitor(void)
{
typedef int (*slsi_mlme_set_fn)(void *sdev, void *dev,
u8 *mib, int mib_len);
slsi_mlme_set_fn mlme_set;
mlme_set = (slsi_mlme_set_fn)kallsyms_lookup_name("slsi_mlme_set");
if (!mlme_set) {
pr_info("rc_wifi_mon: slsi_mlme_set not found — "
"SCSC FW monitor mode MIB not set\n");
pr_info("rc_wifi_mon: monitor mode will work at driver level "
"but FW may filter some frames\n");
return;
}
pr_info("rc_wifi_mon: found slsi_mlme_set, SCSC FW patching "
"available (MIB write deferred to mode switch)\n");
/* Actual MIB write happens when interface is switched to monitor —
* we hook into the mode change path above */
}
/*
* For bcmdhd: set the DHD driver's monitor mode flag and issue
* the firmware iovar to enable monitor.
*/
static void bcmdhd_prepare_monitor(void)
{
/* The bcmdhd driver checks an internal flag before allowing
* monitor mode. We locate dhd_monitor_init or the cfg80211
* vendor command handler.
*
* Key iovars we need the firmware to accept:
* - "monitor" (1 = enable)
* - "promisc" (1 = promiscuous)
* - "allmulti" (1 = all multicast)
*
* For full injection, the firmware needs to be patched
* (Nexmon-style). Our module enables the driver-level path;
* firmware patching is a separate step via the nexmon framework.
*/
pr_info("rc_wifi_mon: bcmdhd driver detected — driver-level "
"monitor mode enabled\n");
pr_info("rc_wifi_mon: for packet injection, Nexmon firmware "
"patch is also required\n");
}
static int __init rc_wifi_mon_init(void)
{
int ret;
pr_info("rc_wifi_mon: RadioControl WiFi monitor mode enabler\n");
detected_driver = detect_driver();
if (detected_driver == DRIVER_UNKNOWN) {
pr_err("rc_wifi_mon: no supported WiFi driver found\n");
return -ENODEV;
}
pr_info("rc_wifi_mon: detected driver type: %d, wiphy: %s\n",
detected_driver,
target_wiphy ? wiphy_name(target_wiphy) : "null");
ret = patch_wiphy();
if (ret) {
pr_err("rc_wifi_mon: failed to patch wiphy: %d\n", ret);
return ret;
}
/* Driver-specific post-patch setup */
switch (detected_driver) {
case DRIVER_SCSC:
scsc_enable_fw_monitor();
break;
case DRIVER_BCMDHD:
bcmdhd_prepare_monitor();
break;
default:
break;
}
pr_info("rc_wifi_mon: loaded — monitor mode available via "
"'iw dev wlanX set type monitor'\n");
return 0;
}
static void __exit rc_wifi_mon_exit(void)
{
/* Restore original handler */
if (target_ops && orig_change_virtual_intf) {
if (make_ops_writable((void *)target_ops, 1) == 0) {
((struct cfg80211_ops *)target_ops)->change_virtual_intf =
orig_change_virtual_intf;
make_ops_writable((void *)target_ops, 0);
}
}
/* Remove monitor from interface_modes */
if (target_wiphy) {
target_wiphy->interface_modes &= ~BIT(NL80211_IFTYPE_MONITOR);
target_wiphy->interface_modes &= ~BIT(NL80211_IFTYPE_OCB);
}
pr_info("rc_wifi_mon: unloaded — monitor mode disabled\n");
}
module_init(rc_wifi_mon_init);
module_exit(rc_wifi_mon_exit);

6
module.prop Normal file
View File

@@ -0,0 +1,6 @@
id=radiocontrol
name=RadioControl
version=v1.0.0
versionCode=1
author=snake
description=Factory field test menus, WiFi radio mode switching (monitor/injection/mesh), hidden radio features, engineering mode & system flags — all from a WebUI.

159
post-fs-data.sh Executable file
View File

@@ -0,0 +1,159 @@
#!/system/bin/sh
# RadioControl — early boot property overrides & kernel module loading
# Runs in post-fs-data context (before zygote)
MODDIR=${0%/*}
#############################
# Load saved configuration
#############################
CONFIG_DIR="/data/adb/radiocontrol"
CONFIG_FILE="$CONFIG_DIR/config.sh"
mkdir -p "$CONFIG_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
cat > "$CONFIG_FILE" << 'DEFAULTS'
# RadioControl configuration — persisted across reboots
# Modified by WebUI, sourced at boot
ENGINEERING_MODE=0
FACTORY_TEST_MODE=0
USB_DIAG_MODE=0
HIDDEN_MENUS=0
MODEM_LOG=0
WIFI_MODE=managed
# Kernel modules to load (space-separated)
# Options: wifi_mon shannon_cmd diag_bridge
LOAD_MODULES=""
DEFAULTS
fi
source "$CONFIG_FILE"
#############################
# Detect chipset
#############################
CHIPSET=$(getprop ro.board.platform 2>/dev/null)
HARDWARE=$(getprop ro.hardware 2>/dev/null)
DETECTED_SOC="unknown"
is_qualcomm() {
case "$CHIPSET" in msm*|sdm*|sm*|qcom*|lahaina|taro|kalama|pineapple|crow) return 0;; esac
case "$HARDWARE" in qcom*) return 0;; esac
return 1
}
is_exynos() {
case "$CHIPSET" in exynos*|universal*|samsungexynos*) return 0;; esac
case "$HARDWARE" in exynos*|samsungexynos*) return 0;; esac
return 1
}
is_tensor() {
case "$CHIPSET" in gs*|zuma*|ripcurrent*|laguna*) return 0;; esac
case "$HARDWARE" in gs*|pixel*|oriole|raven|bluejay|panther|cheetah|lynx|tangorpro|felix|shiba|husky|comet|caiman|komodo|tokay|rango|frankel) return 0;; esac
return 1
}
if is_qualcomm; then DETECTED_SOC="qualcomm"
elif is_exynos; then DETECTED_SOC="exynos"
elif is_tensor; then DETECTED_SOC="tensor"
fi
echo "$DETECTED_SOC" > "$CONFIG_DIR/detected_soc"
#############################
# Apply property overrides
#############################
apply_prop() {
resetprop "$1" "$2" 2>/dev/null
}
if [ "$ENGINEERING_MODE" = "1" ]; then
apply_prop ro.build.type eng
apply_prop ro.debuggable 1
apply_prop persist.sys.usb.config diag,adb
apply_prop ro.secure 0
apply_prop ro.adb.secure 0
apply_prop service.adb.root 1
fi
if [ "$FACTORY_TEST_MODE" = "1" ]; then
apply_prop persist.sys.factorytest 1
apply_prop ro.factorytest 1
apply_prop persist.radio.fieldtest 1
apply_prop ro.telephony.hidden_menu 1
apply_prop persist.radio.apm_sim_not_pwdn 1
apply_prop persist.radio.sib16_support 1
fi
if [ "$HIDDEN_MENUS" = "1" ]; then
apply_prop ro.telephony.hidden_menu 1
apply_prop persist.radio.hidden_menu 1
apply_prop ro.debuggable 1
fi
if [ "$USB_DIAG_MODE" = "1" ]; then
apply_prop persist.sys.usb.config diag,serial_cdev,rmnet,adb
apply_prop sys.usb.configfs 1
fi
if [ "$MODEM_LOG" = "1" ]; then
apply_prop persist.radio.ramdump 1
apply_prop persist.vendor.radio.adb_log_on 1
apply_prop persist.vendor.sys.modem.diag.mdlog on
fi
#############################
# Chipset-specific unlocks
#############################
if [ "$DETECTED_SOC" = "qualcomm" ]; then
if [ "$HIDDEN_MENUS" = "1" ] || [ "$FACTORY_TEST_MODE" = "1" ]; then
apply_prop persist.vendor.radio.adb_log_on 1
apply_prop persist.vendor.radio.enableadvanced 1
apply_prop persist.radio.field_test 1
apply_prop persist.radio.secret_code 1
apply_prop persist.sys.ssr.enable_debug 1
apply_prop persist.vendor.radio.ca_info 1
apply_prop persist.vendor.radio.flexmap_type nw_mode
apply_prop persist.vendor.radio.manual_nw_rej_ct 0
fi
fi
if [ "$DETECTED_SOC" = "exynos" ]; then
if [ "$HIDDEN_MENUS" = "1" ] || [ "$FACTORY_TEST_MODE" = "1" ]; then
apply_prop ro.sec.fle.encryption 0
apply_prop persist.sys.sysdump_mode active
apply_prop persist.sys.usb.q_audio_mod 0
apply_prop ro.factory.sensor 1
apply_prop persist.sys.factorytest 2
apply_prop persist.cp.log 1
apply_prop persist.cp.rat on
fi
fi
if [ "$DETECTED_SOC" = "tensor" ]; then
if [ "$HIDDEN_MENUS" = "1" ] || [ "$FACTORY_TEST_MODE" = "1" ]; then
apply_prop persist.radio.secret_code 1
apply_prop persist.vendor.radio.enableadvanced 1
apply_prop persist.radio.field_test 1
apply_prop persist.vendor.radio.modem_log 1
apply_prop persist.vendor.sys.modem.logging.enable 1
apply_prop persist.vendor.radio.nr5g 1
apply_prop persist.vendor.radio.data_nr_allow 1
fi
fi
#############################
# Mount debugfs if needed
#############################
if [ ! -d /sys/kernel/debug/clk ]; then
mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null
fi

View File

@@ -0,0 +1,26 @@
# RadioControl SELinux rules
# Note: KernelSU can set SELinux to permissive globally.
# These rules are for users who prefer to stay in enforcing mode
# while allowing RadioControl specific access.
# Modem device access (Shannon / Qualcomm)
allow su radio_device chr_file { open read write ioctl getattr }
allow su umts_device chr_file { open read write ioctl getattr }
allow su diag_device chr_file { open read write ioctl getattr }
# debugfs
allow su debugfs dir { search read open getattr }
allow su debugfs file { read write open getattr }
allow su debugfs_wlan dir { search read open getattr }
allow su debugfs_wlan file { read write open getattr }
# sysfs
allow su sysfs_net dir { search read open getattr }
allow su sysfs_net file { read write open getattr setattr }
# Kernel module loading
allow su kernel system { module_load module_request }
# nl80211 for iw commands
allow su self netlink_generic_socket { create bind read write }
allow su self netlink_route_socket { create bind read write nlmsg_write }

242
service.sh Executable file
View File

@@ -0,0 +1,242 @@
#!/system/bin/sh
# RadioControl — late service script
# Loads kernel modules, starts WebUI, applies WiFi mode
MODDIR=${0%/*}
CONFIG_DIR="/data/adb/radiocontrol"
CONFIG_FILE="$CONFIG_DIR/config.sh"
LOG_FILE="$CONFIG_DIR/radiocontrol.log"
PID_FILE="$CONFIG_DIR/webui.pid"
KMOD_DIR="$MODDIR/common/kmod"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
mkdir -p "$CONFIG_DIR"
source "$CONFIG_FILE" 2>/dev/null
DETECTED_SOC=$(cat "$CONFIG_DIR/detected_soc" 2>/dev/null)
log "RadioControl service starting (SoC: $DETECTED_SOC)"
#############################
# Load kernel modules
#############################
load_kmod() {
local mod_name="$1"
local mod_path="$KMOD_DIR/${mod_name}.ko"
if [ -f "$mod_path" ]; then
log "Loading kernel module: $mod_name"
insmod "$mod_path" 2>> "$LOG_FILE"
if [ $? -eq 0 ]; then
log " -> $mod_name loaded successfully"
else
log " -> $mod_name FAILED (may need kernel headers rebuild)"
fi
else
log " -> $mod_name.ko not found at $mod_path"
fi
}
# Auto-detect which modules to load based on SoC
if echo "$LOAD_MODULES" | grep -q "wifi_mon"; then
load_kmod "rc_wifi_mon"
fi
if echo "$LOAD_MODULES" | grep -q "shannon_cmd"; then
load_kmod "rc_shannon_cmd"
elif [ "$DETECTED_SOC" = "exynos" ] || [ "$DETECTED_SOC" = "tensor" ]; then
# Auto-load Shannon module for Exynos/Tensor if AT terminal is used
if [ -c /dev/umts_atc0 ] || [ -c /dev/nr_atc0 ]; then
load_kmod "rc_shannon_cmd"
fi
fi
if echo "$LOAD_MODULES" | grep -q "diag_bridge"; then
load_kmod "rc_diag_bridge"
elif [ "$DETECTED_SOC" = "qualcomm" ] && [ -c /dev/diag ]; then
load_kmod "rc_diag_bridge"
fi
#############################
# Detect available modem interfaces
#############################
detect_modem_interfaces() {
local interfaces=""
# Shannon AT channels (Exynos/Tensor) — umts_router is the primary AT interface
for dev in /dev/umts_router /dev/umts_atc0 /dev/umts_atc1 /dev/umts_router0 /dev/nr_atc0; do
[ -c "$dev" ] && interfaces="$interfaces $dev"
done
# Shannon IPC / DM / OEM channels
for dev in /dev/umts_ipc0 /dev/umts_ipc1 /dev/umts_dm0 /dev/umts_rfs0 \
/dev/umts_boot0 /dev/umts_loopback /dev/umts_rcs0 /dev/umts_rcs1 \
/dev/umts_wfc0 /dev/umts_wfc1 /dev/umts_toe0 \
/dev/oem_ipc0 /dev/oem_ipc1 /dev/oem_ipc2 /dev/oem_ipc3 \
/dev/oem_ipc4 /dev/oem_ipc5 /dev/oem_ipc6 /dev/oem_ipc7 \
/dev/gnss_ipc /dev/acd-factory_diag; do
[ -c "$dev" ] && interfaces="$interfaces $dev"
done
# Qualcomm DIAG
[ -c /dev/diag ] && interfaces="$interfaces /dev/diag"
# Qualcomm AT
for dev in /dev/smd7 /dev/ttyHS0 /dev/ttyMSM0 /dev/at_mdm0; do
[ -c "$dev" ] && interfaces="$interfaces $dev"
done
# Our kernel module devices
[ -c /dev/rc_shannon ] && interfaces="$interfaces /dev/rc_shannon"
[ -c /dev/rc_diag ] && interfaces="$interfaces /dev/rc_diag"
echo "$interfaces" > "$CONFIG_DIR/modem_interfaces"
log "Detected modem interfaces:$interfaces"
}
detect_modem_interfaces
#############################
# Detect WiFi driver & capabilities
#############################
detect_wifi_info() {
local info_file="$CONFIG_DIR/wifi_info"
echo "" > "$info_file"
for iface in /sys/class/net/*; do
local name=$(basename "$iface")
if [ -d "$iface/wireless" ] || [ -d "$iface/phy80211" ] || echo "$name" | grep -qE '^(wlan|wifi|wlp)'; then
echo "IFACE=$name" >> "$info_file"
# Driver name
local driver=$(readlink "$iface/device/driver" 2>/dev/null | xargs basename 2>/dev/null)
echo "DRIVER=$driver" >> "$info_file"
# Module name
local module=$(basename "$(readlink "$iface/device/driver/module" 2>/dev/null)" 2>/dev/null)
echo "MODULE=$module" >> "$info_file"
# Firmware path
if [ -f /sys/module/bcmdhd/parameters/firmware_path ]; then
echo "FW_PATH=$(cat /sys/module/bcmdhd/parameters/firmware_path)" >> "$info_file"
echo "WIFI_CHIP=broadcom" >> "$info_file"
elif [ -d /sys/module/scsc_wlan ]; then
echo "WIFI_CHIP=samsung_scsc" >> "$info_file"
[ -f /d/scsc/mx/mxman ] && echo "SCSC_STATE=$(cat /d/scsc/mx/mxman 2>/dev/null)" >> "$info_file"
elif [ -d /sys/module/ath11k ] || [ -d /sys/module/ath12k ]; then
echo "WIFI_CHIP=qualcomm_ath" >> "$info_file"
elif [ -d /sys/module/cnss2 ]; then
echo "WIFI_CHIP=qualcomm_cnss" >> "$info_file"
fi
# Current mode
local mode=$(iw dev "$name" info 2>/dev/null | grep type | awk '{print $2}')
echo "CURRENT_MODE=$mode" >> "$info_file"
# PHY capabilities
local phy=$(iw dev "$name" info 2>/dev/null | grep wiphy | awk '{print $2}')
if [ -n "$phy" ]; then
echo "PHY=phy${phy}" >> "$info_file"
iw phy "phy${phy}" info 2>/dev/null | sed -n '/Supported interface modes/,/^[^\t]/p' | grep '^\s*\*' | sed 's/.*\* /SUPPORTED_MODE=/' >> "$info_file"
# Bands
iw phy "phy${phy}" info 2>/dev/null | grep -E "Band [0-9]" | sed 's/.*Band /BAND=/' >> "$info_file"
fi
# Check monitor mode via rc_wifi_mon
if lsmod 2>/dev/null | grep -q rc_wifi_mon; then
echo "MONITOR_PATCH=loaded" >> "$info_file"
else
echo "MONITOR_PATCH=not_loaded" >> "$info_file"
fi
fi
done
log "WiFi info written to $info_file"
}
# Wait for WiFi to be up
sleep 3
detect_wifi_info
#############################
# Apply WiFi mode if non-default
#############################
if [ -n "$WIFI_MODE" ] && [ "$WIFI_MODE" != "managed" ]; then
sleep 2
local iface=""
for candidate in wlan0 wlan1 wifi0; do
[ -d "/sys/class/net/$candidate" ] && iface="$candidate" && break
done
if [ -n "$iface" ]; then
log "Applying WiFi mode: $WIFI_MODE on $iface"
ip link set "$iface" down 2>/dev/null
case "$WIFI_MODE" in
monitor) iw dev "$iface" set type monitor 2>/dev/null ;;
injection) iw dev "$iface" set type monitor 2>/dev/null
iw dev "$iface" set monitor fcsfail otherbss 2>/dev/null ;;
mesh) iw dev "$iface" set type mesh 2>/dev/null ;;
ap) iw dev "$iface" set type __ap 2>/dev/null ;;
esac
ip link set "$iface" up 2>/dev/null
fi
fi
#############################
# Enumerate debugfs paths
#############################
enumerate_debugfs() {
local dbg_file="$CONFIG_DIR/debugfs_paths"
echo "" > "$dbg_file"
# Modem/radio related debugfs
for path in \
/sys/kernel/debug/modem_diag \
/sys/kernel/debug/diag \
/sys/kernel/debug/msm_subsys \
/sys/kernel/debug/ipc_logging \
/sys/kernel/debug/cnss \
/sys/kernel/debug/wlan \
/sys/kernel/debug/ath11k \
/sys/kernel/debug/ath12k \
/sys/kernel/debug/ipa \
/sys/kernel/debug/scsc \
/sys/kernel/debug/clk \
/sys/kernel/debug/regulator \
/sys/kernel/debug/remoteproc \
/sys/kernel/debug/aoc \
/sys/kernel/debug/trusty \
/sys/kernel/debug/gsa \
/sys/kernel/debug/mali \
/sys/kernel/debug/asv; do
if [ -d "$path" ]; then
echo "$path" >> "$dbg_file"
fi
done
log "debugfs enumeration done ($(wc -l < "$dbg_file") paths found)"
}
enumerate_debugfs
#############################
# Start WebUI server
#############################
if [ -f "$PID_FILE" ]; then
kill $(cat "$PID_FILE") 2>/dev/null
rm -f "$PID_FILE"
fi
log "Starting WebUI on port 8088"
nohup /system/bin/radiocontrol >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
log "RadioControl service started (PID: $(cat $PID_FILE))"

726
system/bin/radiocontrol Executable file
View File

@@ -0,0 +1,726 @@
#!/system/bin/sh
# RadioControl WebUI — HTTP server with hardware interface APIs
WEBROOT="/data/adb/modules/radiocontrol/webroot"
CONFIG_DIR="/data/adb/radiocontrol"
CONFIG_FILE="$CONFIG_DIR/config.sh"
PORT=8088
mkdir -p "$CONFIG_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [webui] $1"; }
###########################
# Hardware interface helpers
###########################
# Send AT command to modem — tries rc_shannon first, then raw devices
send_at_cmd() {
local cmd="$1"
local timeout="${2:-3}"
local result=""
# Try our kernel module device first
if [ -c /dev/rc_shannon ]; then
echo -e "${cmd}\r" > /dev/rc_shannon
result=$(timeout "$timeout" cat /dev/rc_shannon 2>/dev/null)
[ -n "$result" ] && echo "$result" && return 0
fi
# Try available AT devices — umts_router is confirmed working on Tensor/Shannon 5400
for dev in /dev/umts_router /dev/umts_atc0 /dev/nr_atc0 /dev/umts_atc1 /dev/smd7 /dev/ttyHS0; do
if [ -c "$dev" ]; then
result=$(
exec 3<>"$dev" 2>/dev/null
if [ $? -eq 0 ]; then
printf "${cmd}\r\n" >&3
sleep "$timeout"
local buf=""
while read -t 1 line <&3 2>/dev/null; do
buf="${buf}${line}\n"
done
echo -e "$buf"
exec 3>&-
fi
)
[ -n "$result" ] && echo "$result" && return 0
fi
done
echo "ERROR: No modem AT interface available"
return 1
}
# Read a debugfs or sysfs path safely
read_sys_path() {
local path="$1"
local max_bytes="${2:-4096}"
# Validate path — only allow /sys and /proc
case "$path" in
/sys/kernel/debug/*|/sys/class/*|/sys/devices/*|/sys/module/*|/sys/bus/*|/proc/*)
;;
*)
echo "ERROR: path not allowed"
return 1
;;
esac
if [ -f "$path" ]; then
head -c "$max_bytes" "$path" 2>/dev/null
elif [ -d "$path" ]; then
ls -la "$path" 2>/dev/null
else
echo "ERROR: not found"
return 1
fi
}
# Write to a sysfs path
write_sys_path() {
local path="$1"
local value="$2"
case "$path" in
/sys/class/*|/sys/devices/*|/sys/module/*)
;;
*)
echo "ERROR: write path not allowed"
return 1
;;
esac
if [ -w "$path" ]; then
echo "$value" > "$path" 2>/dev/null && echo "OK" || echo "ERROR: write failed"
else
echo "ERROR: not writable"
return 1
fi
}
# List directory contents as JSON
list_dir_json() {
local dir="$1"
local result="["
local first=1
if [ ! -d "$dir" ]; then
echo "[]"
return
fi
for entry in "$dir"/*; do
[ ! -e "$entry" ] && continue
local name=$(basename "$entry")
local type="file"
[ -d "$entry" ] && type="dir"
local readable="false"
[ -r "$entry" ] && readable="true"
local writable="false"
[ -w "$entry" ] && writable="true"
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"name\":\"$name\",\"type\":\"$type\",\"readable\":$readable,\"writable\":$writable}"
done
echo "${result}]"
}
# Get WiFi interface details
get_wifi_details() {
cat "$CONFIG_DIR/wifi_info" 2>/dev/null
}
# Get loaded kernel modules status
get_kmod_status() {
local result="{"
local mon="false" shan="false" diag="false"
lsmod 2>/dev/null | grep -q rc_wifi_mon && mon="true"
lsmod 2>/dev/null | grep -q rc_shannon_cmd && shan="true"
lsmod 2>/dev/null | grep -q rc_diag_bridge && diag="true"
echo "{\"wifi_mon\":$mon,\"shannon_cmd\":$shan,\"diag_bridge\":$diag}"
}
# Get thermal zones related to radios
get_thermal_info() {
local result="["
local first=1
for tz in /sys/class/thermal/thermal_zone*; do
local type=$(cat "$tz/type" 2>/dev/null)
case "$type" in
*modem*|*pa*|*wlan*|*wifi*|*rf*|*mdm*|*qfe*|*battery*)
local temp=$(cat "$tz/temp" 2>/dev/null)
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"zone\":\"$(basename $tz)\",\"type\":\"$type\",\"temp\":$temp}"
;;
esac
done
echo "${result}]"
}
# Get current config
get_current_config() {
source "$CONFIG_FILE" 2>/dev/null
local soc=$(cat "$CONFIG_DIR/detected_soc" 2>/dev/null)
cat << CONF
{
"engineering_mode": "${ENGINEERING_MODE:-0}",
"factory_test_mode": "${FACTORY_TEST_MODE:-0}",
"usb_diag_mode": "${USB_DIAG_MODE:-0}",
"hidden_menus": "${HIDDEN_MENUS:-0}",
"modem_log": "${MODEM_LOG:-0}",
"wifi_mode": "${WIFI_MODE:-managed}",
"load_modules": "${LOAD_MODULES:-}",
"detected_soc": "$soc"
}
CONF
}
# Get available modem interfaces
get_modem_interfaces() {
local result="["
local first=1
for dev in /dev/umts_atc0 /dev/umts_atc1 /dev/umts_router0 /dev/umts_ipc0 \
/dev/umts_dm0 /dev/umts_boot0 /dev/umts_ramdump0 /dev/umts_rfs0 \
/dev/nr_atc0 /dev/nr_ipc0 \
/dev/diag /dev/smd7 /dev/ttyHS0 /dev/ttyMSM0 /dev/at_mdm0 \
/dev/rc_shannon /dev/rc_diag; do
if [ -c "$dev" ]; then
local perms=$(ls -la "$dev" 2>/dev/null | awk '{print $1}')
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"path\":\"$dev\",\"perms\":\"$perms\"}"
fi
done
echo "${result}]"
}
# Get radio info from props and modem
get_radio_info() {
local baseband=$(getprop gsm.version.baseband 2>/dev/null)
local ril=$(getprop gsm.version.ril-impl 2>/dev/null)
local nettype=$(getprop gsm.network.type 2>/dev/null)
local simstate=$(getprop gsm.sim.state 2>/dev/null)
local operator=$(getprop gsm.operator.alpha 2>/dev/null)
local chipset=$(getprop ro.board.platform 2>/dev/null)
local hardware=$(getprop ro.hardware 2>/dev/null)
local soc=$(cat "$CONFIG_DIR/detected_soc" 2>/dev/null)
cat << INFO
{
"baseband":"$baseband",
"ril":"$ril",
"network_type":"$nettype",
"sim_state":"$simstate",
"operator":"$operator",
"chipset":"$chipset",
"hardware":"$hardware",
"soc_family":"$soc"
}
INFO
}
# Get system flags
get_system_flags() {
local result="["
local first=1
for prop in \
ro.build.type ro.debuggable ro.secure ro.adb.secure \
persist.sys.usb.config persist.sys.factorytest ro.factorytest \
persist.radio.fieldtest ro.telephony.hidden_menu \
persist.radio.hidden_menu persist.mtk.engineer \
persist.vendor.radio.adb_log_on persist.radio.ramdump \
persist.vendor.radio.enableadvanced persist.radio.field_test \
persist.vendor.radio.ca_info persist.vendor.radio.nr5g \
persist.vendor.radio.modem_log persist.cp.log persist.cp.rat \
ro.board.platform ro.hardware ro.hardware.chipname \
gsm.version.baseband gsm.version.ril-impl; do
val=$(getprop "$prop" 2>/dev/null)
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"prop\":\"$prop\",\"value\":\"$val\"}"
done
echo "${result}]"
}
# Update config key
update_config() {
local key="$1" val="$2"
case "$key" in
ENGINEERING_MODE|FACTORY_TEST_MODE|USB_DIAG_MODE|HIDDEN_MENUS|MODEM_LOG)
case "$val" in 0|1) ;; *) echo '{"ok":false,"error":"invalid value"}'; return ;; esac ;;
WIFI_MODE)
case "$val" in managed|monitor|injection|mesh|ap) ;; *) echo '{"ok":false,"error":"invalid mode"}'; return ;; esac ;;
LOAD_MODULES)
;; # freeform
*) echo '{"ok":false,"error":"invalid key"}'; return ;;
esac
if grep -q "^${key}=" "$CONFIG_FILE" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "$CONFIG_FILE"
else
echo "${key}=${val}" >> "$CONFIG_FILE"
fi
echo "{\"ok\":true}"
}
# Set WiFi mode immediately
set_wifi_mode() {
local mode="$1"
local iface=""
for candidate in wlan0 wlan1 wifi0; do
[ -d "/sys/class/net/$candidate" ] && iface="$candidate" && break
done
[ -z "$iface" ] && echo '{"ok":false,"error":"no wifi interface"}' && return
ip link set "$iface" down 2>/dev/null
case "$mode" in
monitor) iw dev "$iface" set type monitor 2>/dev/null ;;
injection) iw dev "$iface" set type monitor 2>/dev/null
iw dev "$iface" set monitor fcsfail otherbss 2>/dev/null ;;
mesh) iw dev "$iface" set type mesh 2>/dev/null ;;
ap) iw dev "$iface" set type __ap 2>/dev/null ;;
managed|*) iw dev "$iface" set type managed 2>/dev/null; mode="managed" ;;
esac
ip link set "$iface" up 2>/dev/null
sed -i "s/^WIFI_MODE=.*/WIFI_MODE=$mode/" "$CONFIG_FILE" 2>/dev/null
echo "{\"ok\":true,\"mode\":\"$mode\",\"iface\":\"$iface\"}"
}
# Get BCM4390 driver parameters
get_wifi_params() {
local result="["
local first=1
local pdir="/sys/module/bcmdhd4390/parameters"
[ ! -d "$pdir" ] && echo "[]" && return
for p in "$pdir"/*; do
local name=$(basename "$p")
local val=$(cat "$p" 2>/dev/null)
local writable="false"
[ -w "$p" ] && writable="true"
# Escape val for JSON
val=$(echo "$val" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"name\":\"$name\",\"value\":\"$val\",\"writable\":$writable}"
done
echo "${result}]"
}
# Set BCM4390 driver parameter
set_wifi_param() {
local name="$1" val="$2"
local path="/sys/module/bcmdhd4390/parameters/$name"
# Validate name (no path traversal)
case "$name" in */*|*..*) echo '{"ok":false,"error":"invalid param name"}'; return ;; esac
if [ -w "$path" ]; then
echo "$val" > "$path" 2>/dev/null
local newval=$(cat "$path" 2>/dev/null)
echo "{\"ok\":true,\"value\":\"$newval\"}"
else
echo "{\"ok\":false,\"error\":\"param not writable\"}"
fi
}
# Get WiFi firmware info
get_wifi_firmware() {
local result="["
local first=1
for fw in /vendor/firmware/fw_bcmdhd* /vendor/firmware/bcmdhd*; do
[ ! -f "$fw" ] && continue
local name=$(basename "$fw")
local size=$(stat -c%s "$fw" 2>/dev/null || ls -la "$fw" | awk '{print $5}')
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"path\":\"$fw\",\"name\":\"$name\",\"size\":$size}"
done
# Current loaded firmware
local cur_fw=$(cat /sys/module/bcmdhd4390/parameters/firmware_path 2>/dev/null)
local cur_nv=$(cat /sys/module/bcmdhd4390/parameters/nvram_path 2>/dev/null)
local info=$(cat /sys/module/bcmdhd4390/parameters/info_string 2>/dev/null | sed 's/"/\\"/g' | tr '\n' ' ')
echo "${result}],\"current_fw\":\"$cur_fw\",\"current_nvram\":\"$cur_nv\",\"info\":\"$info\"}"
}
# Get CP (modem processor) debug info
get_cp_debug() {
local state=$(cat /sys/devices/platform/cpif/modem_state 2>/dev/null)
local pcie=""
local sbb=""
[ -f /sys/devices/platform/cpif/modem/pcie_event_stats ] && \
pcie=$(cat /sys/devices/platform/cpif/modem/pcie_event_stats 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|' | sed 's/|/\\n/g')
[ -f /sys/devices/platform/cpif/modem/sbb_debug ] && \
sbb=$(cat /sys/devices/platform/cpif/modem/sbb_debug 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|' | sed 's/|/\\n/g')
[ -f /sys/devices/platform/cpif/modem/power_stats ] && \
power=$(cat /sys/devices/platform/cpif/modem/power_stats 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|' | sed 's/|/\\n/g')
echo "{\"state\":\"$state\",\"pcie_stats\":\"$pcie\",\"sbb_debug\":\"$sbb\",\"power_stats\":\"$power\"}"
}
# Trigger CP crash dump
trigger_cp_crash() {
if [ -f /sys/devices/platform/cpif/do_cp_crash ]; then
echo 1 > /sys/devices/platform/cpif/do_cp_crash 2>/dev/null
echo '{"ok":true,"msg":"CP crash triggered — ramdump will be saved to /data/vendor/log/cbd/"}'
else
echo '{"ok":false,"error":"do_cp_crash not available"}'
fi
}
# Carrier config — read current settings
get_carrier_config() {
local volte=$(settings get global enhanced_4g_mode_enabled 2>/dev/null)
local wfc=$(settings get global wifi_calling_enabled 2>/dev/null)
local netsel=$(getprop persist.dbg.hide_preferred_network_type 2>/dev/null)
echo "{\"volte\":\"$volte\",\"wfc\":\"$wfc\",\"hide_network_type\":\"$netsel\"}"
}
# Carrier config — set a carrier override
set_carrier_flag() {
local flag="$1" val="$2"
case "$flag" in
volte)
settings put global enhanced_4g_mode_enabled "$val" 2>/dev/null
echo '{"ok":true}'
;;
vonr)
settings put global vonr_enabled "$val" 2>/dev/null
resetprop persist.vendor.radio.vonr_enabled "$val" 2>/dev/null
echo '{"ok":true}'
;;
wfc)
settings put global wifi_calling_enabled "$val" 2>/dev/null
echo '{"ok":true}'
;;
vt)
settings put global vt_ims_enabled "$val" 2>/dev/null
echo '{"ok":true}'
;;
apn)
resetprop persist.dbg.allow_adding_apns 1 2>/dev/null
settings put global allow_adding_apns "$val" 2>/dev/null
echo '{"ok":true}'
;;
nradv)
resetprop persist.vendor.radio.nr_advanced "$val" 2>/dev/null
echo '{"ok":true}'
;;
nettype)
# val=1 means SHOW (so hide=0)
resetprop persist.dbg.hide_preferred_network_type "$( [ "$val" = "1" ] && echo 0 || echo 1)" 2>/dev/null
echo '{"ok":true}'
;;
*) echo '{"ok":false,"error":"unknown flag"}' ;;
esac
}
# List carrier settings protobuf files
get_carrier_files() {
local result="["
local first=1
local csdir="/product/etc/CarrierSettings"
if [ -d "$csdir" ]; then
for f in "$csdir"/*; do
[ ! -f "$f" ] && continue
local name=$(basename "$f")
local size=$(stat -c%s "$f" 2>/dev/null || ls -la "$f" | awk '{print $5}')
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result{\"name\":\"$name\",\"size\":$size}"
done
fi
echo "${result}]"
}
# Dump carrier_config service
dump_carrier_config() {
dumpsys carrier_config 2>/dev/null | head -500
}
# Thread radio (Wonder) info
get_thread_info() {
local result="{"
local has_wonder="false"
if [ -d /sys/class/ieee80211/wonder ]; then
has_wonder="true"
local mac=$(cat /sys/class/ieee80211/wonder/macaddress 2>/dev/null)
local name=$(cat /sys/class/ieee80211/wonder/name 2>/dev/null)
local idx=$(cat /sys/class/ieee80211/wonder/index 2>/dev/null)
result="$result\"present\":true,\"mac\":\"$mac\",\"name\":\"$name\",\"index\":\"$idx\""
# debugfs
local dbg_entries=""
if [ -d /sys/kernel/debug/wonder ]; then
dbg_entries=$(ls /sys/kernel/debug/wonder/ 2>/dev/null | tr '\n' ',' | sed 's/,$//')
fi
result="$result,\"debugfs_entries\":\"$dbg_entries\""
# force_stop_tx status
if [ -f /sys/kernel/debug/wonder/force_stop_tx ]; then
local fstx=$(cat /sys/kernel/debug/wonder/force_stop_tx 2>/dev/null)
result="$result,\"force_stop_tx\":\"$fstx\""
fi
# thread-wpan interface
if [ -d /sys/class/net/thread-wpan ]; then
result="$result,\"wpan_iface\":true"
else
result="$result,\"wpan_iface\":false"
fi
else
result="$result\"present\":false"
fi
echo "${result}}"
}
# Load/unload kernel module
manage_kmod() {
local action="$1" mod="$2"
local kmod_dir="/data/adb/modules/radiocontrol/common/kmod"
case "$mod" in
rc_wifi_mon|rc_shannon_cmd|rc_diag_bridge) ;;
*) echo '{"ok":false,"error":"unknown module"}'; return ;;
esac
if [ "$action" = "load" ]; then
if lsmod 2>/dev/null | grep -q "$mod"; then
echo '{"ok":true,"status":"already loaded"}'
elif [ -f "$kmod_dir/${mod}.ko" ]; then
insmod "$kmod_dir/${mod}.ko" 2>/dev/null
if [ $? -eq 0 ]; then
echo '{"ok":true,"status":"loaded"}'
else
echo '{"ok":false,"error":"insmod failed — kernel version mismatch?"}'
fi
else
echo '{"ok":false,"error":"module .ko not found"}'
fi
elif [ "$action" = "unload" ]; then
rmmod "$mod" 2>/dev/null
echo '{"ok":true,"status":"unloaded"}'
fi
}
###########################
# HTTP server
###########################
get_mime() {
case "$1" in
*.html) echo "text/html" ;; *.css) echo "text/css" ;;
*.js) echo "text/javascript" ;; *.json) echo "application/json" ;;
*) echo "text/plain" ;;
esac
}
send_response() {
local code="$1" ct="$2" body="$3"
printf "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET,POST,OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n%s" \
"$code" "$ct" "${#body}" "$body"
}
send_file() {
local f="$1"
if [ -f "$f" ]; then
send_response "200 OK" "$(get_mime "$f")" "$(cat "$f")"
else
send_response "404 Not Found" "text/plain" "Not Found"
fi
}
# JSON body parser — extract value for key
json_val() {
echo "$1" | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p"
}
handle_request() {
local method path query body content_length
read -r request_line
method=$(echo "$request_line" | awk '{print $1}')
local full_path=$(echo "$request_line" | awk '{print $2}')
path=$(echo "$full_path" | cut -d'?' -f1)
query=$(echo "$full_path" | grep -o '?.*' | cut -c2-)
while read -r header; do
header=$(echo "$header" | tr -d '\r')
[ -z "$header" ] && break
case "$header" in Content-Length:*) content_length=$(echo "$header" | awk '{print $2}') ;; esac
done
body=""
if [ "$method" = "POST" ] && [ -n "$content_length" ] && [ "$content_length" -gt 0 ] 2>/dev/null; then
body=$(dd bs=1 count="$content_length" 2>/dev/null)
fi
# Handle OPTIONS for CORS
if [ "$method" = "OPTIONS" ]; then
send_response "200 OK" "text/plain" ""
return
fi
log "$method $path"
case "$method $path" in
# Static files
"GET /") send_file "$WEBROOT/index.html" ;;
"GET /css/"*|"GET /js/"*) send_file "$WEBROOT$path" ;;
# Status & info APIs
"GET /api/status") send_response "200 OK" "application/json" "$(get_current_config)" ;;
"GET /api/radio") send_response "200 OK" "application/json" "$(get_radio_info)" ;;
"GET /api/flags") send_response "200 OK" "application/json" "$(get_system_flags)" ;;
"GET /api/thermal") send_response "200 OK" "application/json" "$(get_thermal_info)" ;;
"GET /api/kmod") send_response "200 OK" "application/json" "$(get_kmod_status)" ;;
"GET /api/modem/interfaces") send_response "200 OK" "application/json" "$(get_modem_interfaces)" ;;
"GET /api/wifi/info") send_response "200 OK" "text/plain" "$(get_wifi_details)" ;;
"GET /api/wifi/params") send_response "200 OK" "application/json" "$(get_wifi_params)" ;;
"GET /api/wifi/firmware")
local fwdata=$(get_wifi_firmware)
send_response "200 OK" "application/json" "{\"files\":$fwdata}"
;;
"GET /api/cp") send_response "200 OK" "application/json" "$(get_cp_debug)" ;;
"GET /api/carrier/config") send_response "200 OK" "application/json" "$(get_carrier_config)" ;;
"GET /api/carrier/files") send_response "200 OK" "application/json" "$(get_carrier_files)" ;;
"GET /api/carrier/dump")
local dump=$(dump_carrier_config | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' '|' | sed 's/|/\\n/g')
send_response "200 OK" "application/json" "{\"dump\":\"$dump\"}"
;;
"GET /api/thread") send_response "200 OK" "application/json" "$(get_thread_info)" ;;
# AT command terminal
"POST /api/at")
local cmd=$(json_val "$body" "cmd")
local timeout=$(json_val "$body" "timeout")
[ -z "$timeout" ] && timeout=3
local resp=$(send_at_cmd "$cmd" "$timeout")
# Escape for JSON
resp=$(echo "$resp" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' '|' | sed 's/|/\\n/g')
send_response "200 OK" "application/json" "{\"ok\":true,\"response\":\"$resp\"}"
;;
# debugfs/sysfs browser
"POST /api/fs/read")
local fspath=$(json_val "$body" "path")
if [ -d "$fspath" ]; then
local listing=$(list_dir_json "$fspath")
send_response "200 OK" "application/json" "{\"type\":\"dir\",\"entries\":$listing}"
else
local content=$(read_sys_path "$fspath" 8192)
content=$(echo "$content" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' '|' | sed 's/|/\\n/g')
send_response "200 OK" "application/json" "{\"type\":\"file\",\"content\":\"$content\"}"
fi
;;
"POST /api/fs/write")
local fspath=$(json_val "$body" "path")
local fsval=$(json_val "$body" "value")
local result=$(write_sys_path "$fspath" "$fsval")
send_response "200 OK" "application/json" "{\"ok\":true,\"result\":\"$result\"}"
;;
# debugfs paths list
"GET /api/debugfs")
local paths=$(cat "$CONFIG_DIR/debugfs_paths" 2>/dev/null)
local result="["
local first=1
for p in $paths; do
[ -z "$p" ] && continue
if [ "$first" = "1" ]; then first=0; else result="$result,"; fi
result="$result\"$p\""
done
send_response "200 OK" "application/json" "${result}]"
;;
# Config update
"POST /api/config")
local key=$(json_val "$body" "key")
local val=$(json_val "$body" "value")
send_response "200 OK" "application/json" "$(update_config "$key" "$val")"
;;
# WiFi mode switch
"POST /api/wifi/mode")
local mode=$(json_val "$body" "mode")
send_response "200 OK" "application/json" "$(set_wifi_mode "$mode")"
;;
# Kernel module management
"POST /api/kmod/load")
local mod=$(json_val "$body" "module")
send_response "200 OK" "application/json" "$(manage_kmod load "$mod")"
;;
"POST /api/kmod/unload")
local mod=$(json_val "$body" "module")
send_response "200 OK" "application/json" "$(manage_kmod unload "$mod")"
;;
# Carrier config flag
"POST /api/carrier/set")
local flag=$(json_val "$body" "flag")
local val=$(json_val "$body" "value")
send_response "200 OK" "application/json" "$(set_carrier_flag "$flag" "$val")"
;;
# WiFi driver param
"POST /api/wifi/param")
local pname=$(json_val "$body" "name")
local pval=$(json_val "$body" "value")
send_response "200 OK" "application/json" "$(set_wifi_param "$pname" "$pval")"
;;
# CP crash dump
"POST /api/cp/crash")
send_response "200 OK" "application/json" "$(trigger_cp_crash)"
;;
# Set prop at runtime
"POST /api/prop")
local prop=$(json_val "$body" "prop")
local val=$(json_val "$body" "value")
case "$prop" in
persist.*|ro.debuggable|ro.build.type|ro.secure|ro.adb.secure|service.adb.root)
resetprop "$prop" "$val" 2>/dev/null
send_response "200 OK" "application/json" "{\"ok\":true}"
;;
*) send_response "200 OK" "application/json" "{\"ok\":false,\"error\":\"not in allowlist\"}" ;;
esac
;;
# Reboot
"POST /api/reboot")
send_response "200 OK" "application/json" '{"ok":true}'
sleep 1; reboot ;;
*) send_response "404 Not Found" "application/json" '{"error":"not found"}' ;;
esac
}
###########################
# Main
###########################
log "Starting HTTP server on port $PORT"
while true; do
handle_request | busybox nc -ll -p "$PORT" -s 127.0.0.1 2>/dev/null \
|| handle_request | toybox nc -L -p "$PORT" -s 127.0.0.1 2>/dev/null \
|| { log "ERROR: No network listener available"; exit 1; }
done

23
uninstall.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/system/bin/sh
# RadioControl — cleanup on uninstall
CONFIG_DIR="/data/adb/radiocontrol"
PID_FILE="$CONFIG_DIR/webui.pid"
# Stop WebUI server
if [ -f "$PID_FILE" ]; then
kill $(cat "$PID_FILE") 2>/dev/null
fi
# Restore WiFi to managed mode
for iface in wlan0 wlan1 wifi0; do
if [ -d "/sys/class/net/$iface" ]; then
ip link set "$iface" down 2>/dev/null
iw dev "$iface" set type managed 2>/dev/null
ip link set "$iface" up 2>/dev/null
break
fi
done
# Remove persistent config
rm -rf "$CONFIG_DIR"

574
webroot/css/style.css Normal file
View File

@@ -0,0 +1,574 @@
:root {
--bg-primary: #0a0e17;
--bg-card: #111827;
--bg-card-hover: #1a2332;
--bg-input: #1e293b;
--border: #1e3a5f;
--border-active: #3b82f6;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #3b82f6;
--accent-glow: rgba(59, 130, 246, 0.3);
--success: #22c55e;
--success-bg: rgba(34, 197, 94, 0.1);
--warning: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.1);
--danger: #ef4444;
--danger-bg: rgba(239, 68, 68, 0.1);
--radius: 12px;
--radius-sm: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
/* Animated background grid */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 50%, rgba(59,130,246,0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(139,92,246,0.06) 0%, transparent 50%),
linear-gradient(180deg, rgba(59,130,246,0.02) 0%, transparent 40%);
pointer-events: none;
z-index: 0;
}
.app {
position: relative;
z-index: 1;
max-width: 540px;
margin: 0 auto;
padding: 16px;
padding-bottom: 80px;
}
/* Header */
.header {
text-align: center;
padding: 24px 0 20px;
}
.header-icon {
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
box-shadow: 0 8px 32px rgba(59,130,246,0.3);
}
.header-icon svg {
width: 28px;
height: 28px;
fill: white;
}
.header h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #e2e8f0, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
}
/* Status bar */
.status-bar {
display: flex;
gap: 8px;
margin-bottom: 20px;
overflow-x: auto;
scrollbar-width: none;
padding: 2px;
}
.status-bar::-webkit-scrollbar { display: none; }
.status-chip {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
background: var(--bg-card);
border: 1px solid var(--border);
}
.status-chip .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 6px var(--success);
}
.status-chip .dot.warn { background: var(--warning); box-shadow: 0 0 6px var(--warning); }
.status-chip .dot.off { background: var(--text-muted); box-shadow: none; }
/* Section */
.section {
margin-bottom: 16px;
}
.section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-muted);
padding: 0 4px;
margin-bottom: 8px;
}
/* Cards */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 8px;
transition: border-color 0.2s;
}
.card:hover {
border-color: rgba(59,130,246,0.3);
}
.card-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
}
.card-row + .card-row {
border-top: 1px solid var(--border);
}
.card-row-info {
flex: 1;
min-width: 0;
}
.card-row-label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.card-row-desc {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
/* Toggle switch */
.toggle {
position: relative;
width: 44px;
height: 26px;
flex-shrink: 0;
margin-left: 12px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
inset: 0;
background: var(--bg-input);
border-radius: 13px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid var(--border);
}
.toggle-slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
left: 2px;
bottom: 2px;
background: var(--text-muted);
border-radius: 50%;
transition: all 0.3s;
}
.toggle input:checked + .toggle-slider {
background: var(--accent);
border-color: var(--accent);
}
.toggle input:checked + .toggle-slider::before {
transform: translateX(18px);
background: white;
}
/* WiFi mode selector */
.mode-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
padding: 12px 16px 16px;
}
.mode-btn {
padding: 10px 8px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-input);
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn:hover {
border-color: var(--accent);
color: var(--text-primary);
}
.mode-btn.active {
background: rgba(59,130,246,0.15);
border-color: var(--accent);
color: var(--accent);
box-shadow: 0 0 12px rgba(59,130,246,0.2);
}
.mode-btn .mode-icon {
font-size: 18px;
display: block;
margin-bottom: 4px;
}
/* Info grid */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--border);
}
.info-cell {
background: var(--bg-card);
padding: 12px 14px;
}
.info-cell:first-child { border-radius: var(--radius) 0 0 0; }
.info-cell:nth-child(2) { border-radius: 0 var(--radius) 0 0; }
.info-cell:nth-last-child(2) { border-radius: 0 0 0 var(--radius); }
.info-cell:last-child { border-radius: 0 0 var(--radius) 0; }
.info-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-top: 4px;
word-break: break-all;
}
/* Flags table */
.flags-table {
width: 100%;
font-size: 12px;
}
.flags-table tr + tr td {
border-top: 1px solid var(--border);
}
.flags-table td {
padding: 10px 14px;
vertical-align: middle;
}
.flags-table .prop-name {
color: var(--accent);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11px;
white-space: nowrap;
}
.flags-table .prop-value {
color: var(--text-secondary);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11px;
text-align: right;
word-break: break-all;
}
/* Action buttons */
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 16px;
}
.btn {
padding: 12px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.btn:hover {
border-color: var(--accent);
background: var(--bg-card-hover);
}
.btn-danger {
border-color: rgba(239,68,68,0.3);
color: var(--danger);
}
.btn-danger:hover {
background: var(--danger-bg);
border-color: var(--danger);
}
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.btn-primary:hover {
background: #2563eb;
box-shadow: 0 4px 16px rgba(59,130,246,0.3);
}
/* Toast notifications */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(80px);
padding: 12px 20px;
border-radius: var(--radius-sm);
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
opacity: 0;
transition: all 0.3s ease;
z-index: 100;
white-space: nowrap;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.toast.success { border-color: var(--success); }
.toast.error { border-color: var(--danger); }
/* Loading shimmer */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.loading {
background: linear-gradient(90deg, var(--bg-input) 25%, var(--bg-card-hover) 50%, var(--bg-input) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
height: 14px;
width: 60px;
}
/* Tab bar */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(10,14,23,0.95);
backdrop-filter: blur(20px);
border-top: 1px solid var(--border);
display: flex;
justify-content: center;
gap: 4px;
padding: 8px 16px;
padding-bottom: max(8px, env(safe-area-inset-bottom));
z-index: 50;
}
.tab-item {
flex: 1;
max-width: 100px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 6px 8px;
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
background: none;
}
.tab-item:hover { color: var(--text-secondary); }
.tab-item.active {
color: var(--accent);
}
.tab-item svg {
width: 20px;
height: 20px;
fill: currentColor;
}
/* Page visibility */
.page { display: none; }
.page.active { display: block; }
/* AT Terminal */
.terminal-output {
background: #030712;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
max-height: 320px;
overflow-y: auto;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.6;
}
.terminal-input {
flex: 1;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
color: var(--text-primary);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.terminal-input:focus {
border-color: var(--accent);
}
.term-cmd {
color: var(--accent);
padding: 2px 0;
}
.term-prompt {
color: var(--success);
margin-right: 4px;
}
.term-time {
color: var(--text-muted);
font-size: 10px;
margin-left: 8px;
}
.term-resp {
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
padding: 2px 0 6px;
border-bottom: 1px solid rgba(255,255,255,0.04);
margin-bottom: 4px;
}
.term-err {
color: var(--danger);
padding: 2px 0;
}
/* File browser */
.file-content {
background: #030712;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 12px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11px;
line-height: 1.5;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
}
.breadcrumb {
padding: 8px 0 0;
font-size: 12px;
color: var(--text-muted);
}
.crumb {
cursor: pointer;
color: var(--accent);
transition: color 0.2s;
}
.crumb:hover {
color: var(--text-primary);
}
/* Responsive */
@media (max-width: 380px) {
.mode-grid { grid-template-columns: repeat(2, 1fr); }
.info-grid { grid-template-columns: 1fr; }
.actions { grid-template-columns: 1fr; }
}

386
webroot/index.html Normal file
View File

@@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#0a0e17">
<title>RadioControl</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="app">
<div class="header">
<div class="header-icon">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
</div>
<h1>RadioControl</h1>
<p id="soc-badge">Detecting SoC...</p>
</div>
<div class="status-bar">
<div class="status-chip" id="chip-eng"><span class="dot off"></span> ENG</div>
<div class="status-chip" id="chip-factory"><span class="dot off"></span> FTM</div>
<div class="status-chip" id="chip-wifi"><span class="dot off"></span> WiFi</div>
<div class="status-chip" id="chip-kmod"><span class="dot off"></span> KMod</div>
</div>
<!-- ==================== RADIO PAGE ==================== -->
<div class="page active" id="page-radio">
<div class="section">
<div class="section-title">Mode Switches</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Engineering Mode</div>
<div class="card-row-desc">build.type=eng, root ADB, diag USB</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-eng"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Factory Field Test</div>
<div class="card-row-desc">Radio field test, hidden band info, factory props</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-factory"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Hidden Menus</div>
<div class="card-row-desc">ServiceMode, SysDump, Shannon debug</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-hidden"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">USB Diagnostics</div>
<div class="card-row-desc">diag,serial_cdev,rmnet — Shannon DM over USB</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-diag"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Modem Logging</div>
<div class="card-row-desc">Ramdump, DIAG log, mdlog, CP log</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-modem"><span class="toggle-slider"></span></label>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Radio Info</div>
<div class="info-grid" style="border:1px solid var(--border);border-radius:var(--radius)">
<div class="info-cell"><div class="info-label">SoC Family</div><div class="info-value" id="info-soc">-</div></div>
<div class="info-cell"><div class="info-label">Baseband</div><div class="info-value" id="info-baseband">-</div></div>
<div class="info-cell"><div class="info-label">Platform</div><div class="info-value" id="info-chipset">-</div></div>
<div class="info-cell"><div class="info-label">Network</div><div class="info-value" id="info-network">-</div></div>
<div class="info-cell"><div class="info-label">Operator</div><div class="info-value" id="info-operator">-</div></div>
<div class="info-cell"><div class="info-label">SIM</div><div class="info-value" id="info-sim">-</div></div>
</div>
</div>
<div class="section">
<div class="section-title">Modem Interfaces</div>
<div class="card" id="modem-interfaces"><div class="card-row"><div class="card-row-desc">Scanning...</div></div></div>
</div>
<div class="section">
<div class="section-title">RF Thermal</div>
<div class="card" id="thermal-info"><div class="card-row"><div class="card-row-desc">Loading...</div></div></div>
</div>
<div class="section">
<div class="section-title">CP (Modem Processor) Debug</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Modem State</div>
<div class="card-row-desc" id="cp-state"></div>
</div>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">PCIe Stats</div>
<div class="card-row-desc" id="cp-pcie"></div>
</div>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">SBB Debug</div>
<div class="card-row-desc" id="cp-sbb"></div>
</div>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Trigger CP Crash Dump</div>
<div class="card-row-desc">Write to /sys/devices/platform/cpif/do_cp_crash for modem ramdump analysis</div>
</div>
<button class="btn btn-danger" style="padding:6px 12px;font-size:12px" onclick="triggerCPCrash()">Dump</button>
</div>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" onclick="loadRadio()">Refresh</button>
<button class="btn btn-danger" onclick="reboot()">Reboot</button>
</div>
</div>
<!-- ==================== AT TERMINAL ==================== -->
<div class="page" id="page-terminal">
<div class="section">
<div class="section-title">AT Command Terminal</div>
<div class="card" style="padding:12px 16px">
<div class="card-row-desc" style="margin-bottom:8px">
Direct AT interface to Shannon 5400 modem via /dev/umts_router.
</div>
<div id="at-history" class="terminal-output"></div>
<div style="display:flex;gap:8px;margin-top:8px">
<input type="text" id="at-input" class="terminal-input" placeholder="AT+CFUN?" spellcheck="false"
onkeydown="if(event.key==='Enter')sendATCmd()">
<button class="btn btn-primary" style="flex-shrink:0;padding:8px 16px" onclick="sendATCmd()">Send</button>
</div>
</div>
</div>
<div class="section">
<div class="section-title" id="at-presets-title">Quick Commands</div>
<div id="at-presets" class="card"></div>
</div>
<div class="section">
<div class="section-title">Discovered Commands — Shannon 5400 (S5400BUNUELO)</div>
<div id="discovered-commands"></div>
</div>
</div>
<!-- ==================== WIFI PAGE ==================== -->
<div class="page" id="page-wifi">
<div class="section">
<div class="section-title">Kernel Modules</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">rc_wifi_mon</div>
<div class="card-row-desc">Runtime monitor/injection mode patch for cfg80211</div>
</div>
<button class="btn" id="btn-kmod-wifi" style="padding:6px 12px;font-size:12px" onclick="toggleKmod('rc_wifi_mon')">Load</button>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">rc_shannon_cmd</div>
<div class="card-row-desc">Shannon modem direct AT bypass (/dev/rc_shannon)</div>
</div>
<button class="btn" id="btn-kmod-shannon" style="padding:6px 12px;font-size:12px" onclick="toggleKmod('rc_shannon_cmd')">Load</button>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">rc_diag_bridge</div>
<div class="card-row-desc">Qualcomm DIAG/NV item bridge (/dev/rc_diag)</div>
</div>
<button class="btn" id="btn-kmod-diag" style="padding:6px 12px;font-size:12px" onclick="toggleKmod('rc_diag_bridge')">Load</button>
</div>
</div>
</div>
<div class="section">
<div class="section-title">WiFi Radio Mode</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Select Mode</div>
<div class="card-row-desc">Requires rc_wifi_mon for monitor/injection on BCM4390</div>
</div>
</div>
<div class="mode-grid">
<button class="mode-btn active" data-mode="managed">Managed</button>
<button class="mode-btn" data-mode="monitor">Monitor</button>
<button class="mode-btn" data-mode="injection">Injection</button>
<button class="mode-btn" data-mode="mesh">Mesh</button>
<button class="mode-btn" data-mode="ap">AP</button>
</div>
</div>
</div>
<div class="section">
<div class="section-title">BCM4390 Driver Parameters</div>
<div class="card" id="wifi-params"><div class="card-row"><div class="card-row-desc">Loading...</div></div></div>
</div>
<div class="section">
<div class="section-title">WiFi Firmware</div>
<div class="card" id="wifi-firmware"><div class="card-row"><div class="card-row-desc">Loading...</div></div></div>
</div>
<div class="section">
<div class="section-title">WiFi Hardware</div>
<div class="card" id="wifi-hw-info"><div class="card-row"><div class="card-row-desc">Detecting...</div></div></div>
</div>
</div>
<!-- ==================== CARRIER PAGE ==================== -->
<div class="page" id="page-carrier">
<div class="section">
<div class="section-title">Carrier Config Override</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Force VoLTE</div>
<div class="card-row-desc">carrier_volte_available_bool — enable on any carrier</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-volte"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Force VoNR (5G Voice)</div>
<div class="card-row-desc">Enable voice over NR standalone</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-vonr"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Force WiFi Calling</div>
<div class="card-row-desc">carrier_wfc_ims_available_bool</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-wfc"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Force Video Calling</div>
<div class="card-row-desc">carrier_vt_available_bool</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-vt"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Allow Custom APNs</div>
<div class="card-row-desc">allow_adding_apns_bool — unlock APN editing</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-apn"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">NR Advanced (mmWave icon)</div>
<div class="card-row-desc">nr_advanced_capable_carrier_bool</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-nradv"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Show Network Type Selector</div>
<div class="card-row-desc">Unhide preferred network type in settings</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-nettype"><span class="toggle-slider"></span></label>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Carrier Settings Files</div>
<div class="card" id="carrier-files"><div class="card-row"><div class="card-row-desc">Scanning /product/etc/CarrierSettings/...</div></div></div>
</div>
<div class="section">
<div class="section-title">Current Carrier Config Dump</div>
<div class="card" style="padding:12px 16px">
<button class="btn" onclick="dumpCarrierConfig()" style="width:100%">Run: dumpsys carrier_config</button>
<pre class="file-content" id="carrier-dump" style="margin-top:8px;display:none"></pre>
</div>
</div>
</div>
<!-- ==================== DEBUGFS BROWSER ==================== -->
<div class="page" id="page-debug">
<div class="section">
<div class="section-title">debugfs / sysfs Browser</div>
<div class="card" style="padding:12px 16px">
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="fs-path" class="terminal-input" value="/sys/kernel/debug" placeholder="/sys/kernel/debug"
onkeydown="if(event.key==='Enter')browsePath()">
<button class="btn" style="flex-shrink:0;padding:8px 12px" onclick="browsePath()">Go</button>
<button class="btn" style="flex-shrink:0;padding:8px 12px" onclick="fsUp()">Up</button>
</div>
<div id="fs-breadcrumb" class="breadcrumb"></div>
</div>
</div>
<div class="section">
<div id="fs-content" class="card">
<div class="card-row"><div class="card-row-desc">Enter a path above</div></div>
</div>
</div>
<div class="section">
<div class="section-title">Quick Paths</div>
<div class="card" id="debugfs-shortcuts"></div>
</div>
<div class="section">
<div class="section-title">Thread / 802.15.4 Radio (Wonder)</div>
<div class="card" id="thread-info"><div class="card-row"><div class="card-row-desc">Loading...</div></div></div>
</div>
<div class="section">
<div class="section-title">Satellite / NTN (Skylo)</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">AT+SKYLOTEST</div>
<div class="card-row-desc">Non-terrestrial network test mode — satellite IoT connectivity</div>
</div>
<button class="btn btn-primary" style="padding:6px 12px;font-size:12px" onclick="runPreset('AT+SKYLOTEST')">Run</button>
</div>
</div>
</div>
</div>
<!-- ==================== FLAGS PAGE ==================== -->
<div class="page" id="page-flags">
<div class="section">
<div class="section-title">System Properties</div>
<div class="card">
<table class="flags-table"><tbody id="flags-body">
<tr><td class="prop-name" colspan="2">Loading...</td></tr>
</tbody></table>
</div>
</div>
<div class="actions" style="grid-template-columns:1fr">
<button class="btn" onclick="loadFlags()">Refresh</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<div class="tab-bar">
<button class="tab-item active" data-tab="radio">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
Radio
</button>
<button class="tab-item" data-tab="terminal">
<svg viewBox="0 0 24 24"><path d="M20 19V7H4v12h16m0-16a2 2 0 012 2v14a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h16M7 9l3.5 3.5L7 16l1 1 4.5-4.5L8 8l-1 1m5 7h5v1h-5v-1z"/></svg>
AT
</button>
<button class="tab-item" data-tab="wifi">
<svg viewBox="0 0 24 24"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/></svg>
WiFi
</button>
<button class="tab-item" data-tab="carrier">
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z"/></svg>
Carrier
</button>
<button class="tab-item" data-tab="debug">
<svg viewBox="0 0 24 24"><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>
Debug
</button>
<button class="tab-item" data-tab="flags">
<svg viewBox="0 0 24 24"><path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/></svg>
Flags
</button>
</div>
<script src="/js/app.js"></script>
</body>
</html>

828
webroot/js/app.js Normal file
View File

@@ -0,0 +1,828 @@
// RadioControl WebUI — Hardware Interface Controller
let config = {};
let currentTab = 'radio';
let toastTimer = null;
let atHistory = [];
// ---- API ----
async function api(path, opts = {}) {
try {
const res = await fetch(path, opts);
return await res.json();
} catch (e) {
showToast('Connection error', 'error');
return null;
}
}
const post = (path, body) => api(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
// ---- Toast ----
function showToast(msg, type = 'success') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = 'toast ' + type + ' show';
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.className = 'toast', 2500);
}
// ---- Tabs ----
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
document.getElementById('page-' + tab).classList.add('active');
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
refreshTab(tab);
}
async function refreshTab(tab) {
switch (tab) {
case 'radio': await loadRadio(); await loadCPDebug(); break;
case 'terminal': await loadTerminal(); break;
case 'wifi': await loadWifi(); break;
case 'carrier': await loadCarrier(); break;
case 'debug': await loadDebug(); await loadThreadInfo(); break;
case 'flags': await loadFlags(); break;
}
}
// ---- Radio page ----
async function loadRadio() {
config = await api('/api/status') || config;
const radio = await api('/api/radio');
setToggle('toggle-eng', config.engineering_mode);
setToggle('toggle-factory', config.factory_test_mode);
setToggle('toggle-hidden', config.hidden_menus);
setToggle('toggle-diag', config.usb_diag_mode);
setToggle('toggle-modem', config.modem_log);
updateChips();
if (radio) {
setText('info-soc', radio.soc_family || 'unknown');
setText('info-baseband', radio.baseband || 'N/A');
setText('info-chipset', radio.chipset || 'N/A');
setText('info-network', radio.network_type || 'N/A');
setText('info-operator', radio.operator || 'N/A');
setText('info-sim', radio.sim_state || 'N/A');
setText('soc-badge', `${(radio.soc_family || 'unknown').toUpperCase()}${radio.chipset || '?'}`);
}
// Modem interfaces
const ifaces = await api('/api/modem/interfaces');
const mc = document.getElementById('modem-interfaces');
if (ifaces && ifaces.length > 0) {
mc.innerHTML = ifaces.map(i => `
<div class="card-row">
<div class="card-row-info">
<span class="prop-name">${i.path}</span>
<div class="card-row-desc">${i.perms}</div>
</div>
</div>`).join('');
} else {
mc.innerHTML = '<div class="card-row"><div class="card-row-desc">No modem interfaces detected</div></div>';
}
// Thermal
const thermal = await api('/api/thermal');
const tc = document.getElementById('thermal-info');
if (thermal && thermal.length > 0) {
tc.innerHTML = thermal.map(t => {
const tempC = (t.temp / 1000).toFixed(1);
const color = t.temp > 60000 ? 'var(--danger)' : t.temp > 45000 ? 'var(--warning)' : 'var(--success)';
return `<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">${t.type}</div>
<div class="card-row-desc">${t.zone}</div>
</div>
<span style="color:${color};font-weight:600;font-family:monospace">${tempC}&deg;C</span>
</div>`;
}).join('');
} else {
tc.innerHTML = '<div class="card-row"><div class="card-row-desc">No RF thermal zones</div></div>';
}
}
function setToggle(id, val) {
const el = document.getElementById(id);
if (el) el.checked = val === '1' || val === 1;
}
function setText(id, val) {
const el = document.getElementById(id);
if (el) el.textContent = val;
}
function updateChips() {
setChip('chip-eng', config.engineering_mode === '1');
setChip('chip-factory', config.factory_test_mode === '1');
setChip('chip-wifi', config.wifi_mode !== 'managed' && config.wifi_mode);
}
function setChip(id, on) {
const dot = document.querySelector(`#${id} .dot`);
if (dot) dot.className = 'dot ' + (on ? '' : 'off');
}
async function handleToggle(key, el) {
const val = el.checked ? '1' : '0';
const res = await post('/api/config', { key, value: val });
if (res && res.ok) {
config[key.toLowerCase()] = val;
updateChips();
showToast('Updated — reboot to apply');
} else {
el.checked = !el.checked;
showToast('Failed', 'error');
}
}
async function reboot() {
if (confirm('Reboot to apply all changes?')) {
await post('/api/reboot', {});
showToast('Rebooting...');
}
}
// ---- AT Terminal ----
// AT command presets — verified against Shannon 5400 (S5400BUNUELO) AT+CLAC
// Pixel 10 Pro Fold / Tensor G5 / laguna
const AT_PRESETS = {
common: [
{ cmd: 'AT', desc: 'Modem alive check' },
{ cmd: 'AT+CFUN?', desc: 'Radio function state (0=off, 1=on, 4=airplane)' },
{ cmd: 'AT+CFUN=1', desc: 'Turn radio ON' },
{ cmd: 'AT+CFUN=0', desc: 'Turn radio OFF' },
{ cmd: 'AT+COPS?', desc: 'Current operator' },
{ cmd: 'AT+COPS=?', desc: 'Scan all available networks (slow ~30s)' },
{ cmd: 'AT+CSQ', desc: 'Signal quality (RSSI, BER)' },
{ cmd: 'AT+CESQ', desc: 'Extended signal: RXLEV,BER,RSCP,Ec/No,RSRQ,RSRP,SINR' },
{ cmd: 'AT+CREG?', desc: '2G/3G registration status' },
{ cmd: 'AT+CEREG?', desc: 'LTE registration status' },
{ cmd: 'AT+CGREG?', desc: 'GPRS registration status' },
{ cmd: 'AT+CGDCONT?', desc: 'PDP context / APN list' },
{ cmd: 'AT+CGPADDR', desc: 'PDP addresses (IP assignments)' },
{ cmd: 'AT+CGATT?', desc: 'PS attach status' },
{ cmd: 'AT+CPIN?', desc: 'SIM status (READY/PIN/PUK/not inserted)' },
{ cmd: 'AT+CGSN', desc: 'IMEI(s)' },
{ cmd: 'AT+CIMI', desc: 'IMSI (SIM identity)' },
{ cmd: 'AT+CNUM', desc: 'Subscriber number (phone number)' },
{ cmd: 'AT+CGMI', desc: 'Manufacturer (Samsung Electronics)' },
{ cmd: 'AT+CGMM', desc: 'Model (S5400BUNUELO)' },
{ cmd: 'AT+CGMR', desc: 'Firmware revision' },
{ cmd: 'AT+CLAC', desc: 'List ALL supported AT commands' },
{ cmd: 'AT+WS46?', desc: 'Wireless data service type' },
{ cmd: 'AT+CEMODE?', desc: 'UE mode of operation (CS/PS)' },
],
// Shannon 5400 specific commands (verified on device)
tensor: [
{ cmd: 'AT+SCELL', desc: 'Serving cell info (band, EARFCN, PCI, RSRP)' },
{ cmd: 'AT+NCELL', desc: 'Neighbor cell list' },
{ cmd: 'AT+RSRP', desc: 'Reference Signal Received Power' },
{ cmd: 'AT+RSRQ', desc: 'Reference Signal Received Quality' },
{ cmd: 'AT+RSCP', desc: 'Received Signal Code Power (3G)' },
{ cmd: 'AT+ECNO', desc: 'Ec/No (3G signal quality)' },
{ cmd: 'AT$RSRP', desc: 'Extended RSRP readout' },
{ cmd: 'AT$RSRQ', desc: 'Extended RSRQ readout' },
{ cmd: 'AT$CSQ', desc: 'Extended signal quality' },
{ cmd: 'AT$CREG', desc: 'Extended registration info' },
{ cmd: 'AT$ROAM', desc: 'Roaming status/config' },
{ cmd: 'AT$ARME', desc: 'ARM Engine — modem subsystem control' },
{ cmd: 'AT$ARMEE', desc: 'ARM Engine extended' },
{ cmd: 'AT*CNTI', desc: 'Current Network Technology Indicator' },
{ cmd: 'AT+NBLTESCAN', desc: 'NB-IoT/LTE cell scan' },
{ cmd: 'AT+DEVFEAT', desc: 'Device feature flags' },
{ cmd: 'AT+HANDLEDRX', desc: 'DRX (Discontinuous Reception) control' },
{ cmd: 'AT+PING', desc: 'Modem-level ping test' },
{ cmd: 'AT+PACSP?', desc: 'Preferred ACSP setting' },
{ cmd: 'AT+SKYLOTEST', desc: 'Satellite IoT (Skylo/NTN) test mode' },
{ cmd: 'AT+CPSMS?', desc: 'Power Saving Mode settings' },
{ cmd: 'AT+CCIOTOPT?', desc: 'CIoT optimization config' },
{ cmd: 'AT+CGEQOS?', desc: 'EPS QoS parameters' },
{ cmd: 'AT+C5GQOS?', desc: '5G QoS parameters' },
{ cmd: 'AT+C5GNSSAI?', desc: '5G Network Slice (NSSAI) config' },
{ cmd: 'AT+C5GPNSSAI?', desc: '5G Preferred NSSAI' },
{ cmd: 'AT+C5GUSMS?', desc: '5G USMS config' },
{ cmd: 'AT+CSUPI?', desc: 'SUPI (5G subscriber permanent identity)' },
{ cmd: 'AT+CVMOD?', desc: 'Voice mode preference' },
{ cmd: 'AT+AIMSCH', desc: 'IMS channel status' },
{ cmd: 'AT+AIMSDB', desc: 'IMS debug info' },
{ cmd: 'AT+AIMSRX', desc: 'IMS receive status' },
{ cmd: 'AT+VZWRSRP', desc: 'Verizon RSRP (carrier-specific signal)' },
{ cmd: 'AT+VZWRSRQ', desc: 'Verizon RSRQ' },
{ cmd: 'AT+VZWAPNE?', desc: 'Verizon APN entries' },
{ cmd: 'AT+SKT', desc: 'SK Telecom specific query' },
{ cmd: 'AT+CEER', desc: 'Extended error report (last call/data failure reason)' },
],
// Qualcomm still relevant for other devices
qualcomm: [
{ cmd: 'AT+QENG="servingcell"', desc: 'Serving cell engineering info' },
{ cmd: 'AT+QENG="neighbourcell"', desc: 'Neighbor cell list' },
{ cmd: 'AT+QCAINFO', desc: 'Active carrier aggregation status' },
{ cmd: 'AT+QNWPREFCFG="mode_pref"', desc: 'Network mode preference' },
{ cmd: 'AT+QNWPREFCFG="lte_band"', desc: 'LTE band preference' },
{ cmd: 'AT+QNWPREFCFG="nr5g_band"', desc: 'NR band preference' },
{ cmd: 'AT+QCFG="band"', desc: 'Band lock config (hex bitmask)' },
{ cmd: 'AT+QRSRP', desc: 'Per-antenna RSRP' },
{ cmd: 'AT+QMBNCFG="list"', desc: 'MBN carrier config profiles' },
{ cmd: 'AT+QNWLOCK="common/4g"', desc: 'Cell lock status' },
{ cmd: 'AT+QSCAN=1', desc: 'Quick cell scan' },
],
exynos: [
{ cmd: 'AT+SCELL', desc: 'Serving cell info' },
{ cmd: 'AT+NCELL', desc: 'Neighbor cells' },
{ cmd: 'AT+RSRP', desc: 'RSRP' },
{ cmd: 'AT+RSRQ', desc: 'RSRQ' },
{ cmd: 'AT$RSRP', desc: 'Extended RSRP' },
{ cmd: 'AT$RSRQ', desc: 'Extended RSRQ' },
{ cmd: 'AT$ARME', desc: 'ARM Engine control' },
{ cmd: 'AT*CNTI', desc: 'Network technology indicator' },
{ cmd: 'AT+NBLTESCAN', desc: 'Cell scan' },
{ cmd: 'AT+DEVFEAT', desc: 'Device features' },
],
};
// Full Shannon 5400 AT command reference — verified via AT+CLAC on Pixel 10 Pro Fold
// Modem: S5400BUNUELO, Firmware: g5400i-251201-260127-B-14784805
// RIL: Samsung S.LSI Vendor RIL 5400 V2.3
const DISCOVERED_COMMANDS = {
'Signal & Cell Info': [
{ cmd: 'AT+SCELL', desc: 'Serving cell info — band, EARFCN, PCI, RSRP, timing advance' },
{ cmd: 'AT+NCELL', desc: 'Neighbor cell list with signal levels' },
{ cmd: 'AT+RSRP', desc: 'Reference Signal Received Power (LTE/NR)' },
{ cmd: 'AT+RSRQ', desc: 'Reference Signal Received Quality (LTE/NR)' },
{ cmd: 'AT+RSCP', desc: 'Received Signal Code Power (3G WCDMA)' },
{ cmd: 'AT+ECNO', desc: 'Ec/No ratio (3G signal quality metric)' },
{ cmd: 'AT$RSRP', desc: 'Extended RSRP — detailed per-antenna/per-carrier readout' },
{ cmd: 'AT$RSRQ', desc: 'Extended RSRQ — detailed per-antenna/per-carrier readout' },
{ cmd: 'AT$CSQ', desc: 'Extended signal quality beyond standard +CSQ' },
{ cmd: 'AT+CESQ', desc: 'Extended signal: RXLEV, BER, RSCP, Ec/No, RSRQ, RSRP, SINR (all in one)' },
{ cmd: 'AT+CSQ', desc: 'Basic signal quality — RSSI (0-31) and BER (0-7)' },
{ cmd: 'AT+NBLTESCAN', desc: 'NB-IoT / LTE cell scan — finds all visible cells' },
{ cmd: 'AT*CNTI', desc: 'Current Network Technology Indicator (GSM/WCDMA/LTE/NR)' },
],
'Registration & Network': [
{ cmd: 'AT+CREG?', desc: 'CS (circuit-switched) registration — 2G/3G voice' },
{ cmd: 'AT+CGREG?', desc: 'PS (packet-switched) registration — GPRS/EDGE data' },
{ cmd: 'AT+CEREG?', desc: 'EPS registration — LTE status, TAC, cell ID' },
{ cmd: 'AT$CREG', desc: 'Extended registration with extra detail' },
{ cmd: 'AT+COPS?', desc: 'Current operator (MCC/MNC, access tech)' },
{ cmd: 'AT+COPS=?', desc: 'Scan all networks (~30 seconds, returns PLMN list)' },
{ cmd: 'AT$ROAM', desc: 'Roaming status and configuration' },
{ cmd: 'AT+COPN', desc: 'Read operator name lookup table' },
],
'Radio Control': [
{ cmd: 'AT+CFUN=1', desc: 'Turn radio ON (full functionality)' },
{ cmd: 'AT+CFUN=0', desc: 'Turn radio OFF (minimum functionality)' },
{ cmd: 'AT+CFUN=4', desc: 'Airplane mode (TX disabled, some RX may work)' },
{ cmd: 'AT+CFUN?', desc: 'Query current radio state' },
{ cmd: 'AT+WS46?', desc: 'Wireless data service type selection' },
{ cmd: 'AT+CEMODE?', desc: 'UE mode of operation — CS only, PS only, CS+PS' },
{ cmd: 'AT+CVMOD?', desc: 'Voice mode preference (CS voice vs VoLTE)' },
{ cmd: 'AT+CGATT=1', desc: 'PS attach (connect to data)' },
{ cmd: 'AT+CGATT=0', desc: 'PS detach (disconnect data)' },
{ cmd: 'AT+HANDLEDRX', desc: 'DRX (Discontinuous Reception) control — affects power/latency' },
{ cmd: 'AT+CPSMS?', desc: 'Power Saving Mode config (for IoT/battery optimization)' },
{ cmd: 'AT+CCIOTOPT?', desc: 'CIoT EPS optimization — control plane vs user plane' },
],
'5G / NR / Network Slicing': [
{ cmd: 'AT+C5GNSSAI?', desc: '5G network slice selection (S-NSSAI list)' },
{ cmd: 'AT+C5GNSSAIRDP', desc: 'Read dynamic 5G NSSAI parameters' },
{ cmd: 'AT+C5GPNSSAI?', desc: '5G preferred NSSAI configuration' },
{ cmd: 'AT+C5GQOS?', desc: '5G QoS flow parameters (5QI, MBR, GBR)' },
{ cmd: 'AT+C5GUSMS?', desc: '5G USMS (User Services Management) config' },
{ cmd: 'AT+CSUPI?', desc: 'SUPI — 5G Subscription Permanent Identifier' },
],
'Data / APN / Bearer': [
{ cmd: 'AT+CGDCONT?', desc: 'PDP context list — all configured APNs' },
{ cmd: 'AT+CGPADDR', desc: 'PDP addresses — current IP assignments per CID' },
{ cmd: 'AT+CGACT?', desc: 'PDP context activation status' },
{ cmd: 'AT+CGCONTRDP', desc: 'Read dynamic PDP context parameters' },
{ cmd: 'AT+CGEQOS?', desc: 'EPS bearer QoS parameters' },
{ cmd: 'AT+CGEQOSRDP', desc: 'Read dynamic EPS QoS' },
{ cmd: 'AT+CGTFT?', desc: 'Traffic Flow Template — packet filters' },
{ cmd: 'AT+CGTFTRDP', desc: 'Read dynamic TFT parameters' },
{ cmd: 'AT+CGAUTH?', desc: 'PDP context authentication (PAP/CHAP)' },
{ cmd: 'AT+CGDSCONT?', desc: 'Secondary PDP context definitions' },
{ cmd: 'AT+CGSCONTRDP', desc: 'Read dynamic secondary PDP params' },
{ cmd: 'AT+CGEREP?', desc: 'Packet domain event reporting' },
{ cmd: 'AT+CRTDCP?', desc: 'Reporting of terminating data via control plane' },
{ cmd: 'AT+CSODCP?', desc: 'Sending originating data via control plane' },
{ cmd: 'AT+PING', desc: 'Modem-level ping test (bypasses Android IP stack)' },
],
'IMS (IP Multimedia Subsystem)': [
{ cmd: 'AT+AIMSCH', desc: 'IMS channel status' },
{ cmd: 'AT+AIMSCH8', desc: 'IMS channel status (extended/8-bit)' },
{ cmd: 'AT+AIMSCY', desc: 'IMS cycle/session info' },
{ cmd: 'AT+AIMSCY8', desc: 'IMS cycle info (extended)' },
{ cmd: 'AT+AIMSDB', desc: 'IMS debug information dump' },
{ cmd: 'AT+AIMSDEREG', desc: 'IMS deregistration — force disconnect from IMS' },
{ cmd: 'AT+AIMSRX', desc: 'IMS receive status/config' },
{ cmd: 'AT+AIMSTR', desc: 'IMS transfer/transaction info' },
],
'SIM & Identity': [
{ cmd: 'AT+CPIN?', desc: 'SIM status — READY, SIM PIN, SIM PUK, not inserted' },
{ cmd: 'AT+CGSN', desc: 'IMEI(s) — both slots on dual-SIM' },
{ cmd: 'AT+CIMI', desc: 'IMSI — subscriber identity from SIM' },
{ cmd: 'AT+CNUM', desc: 'Subscriber phone number from SIM' },
{ cmd: 'AT+CPLS?', desc: 'Preferred PLMN list selector' },
{ cmd: 'AT+CPOL?', desc: 'Preferred operator list from SIM' },
{ cmd: 'AT+CUAD', desc: 'UICC application discovery' },
{ cmd: 'AT+CCHO', desc: 'Open logical channel to UICC app' },
{ cmd: 'AT+CCHC', desc: 'Close logical channel' },
{ cmd: 'AT+CGLA', desc: 'Generic UICC logical channel access (raw APDU)' },
{ cmd: 'AT+CRSM', desc: 'Restricted SIM access (read/write SIM files)' },
{ cmd: 'AT+CSIM', desc: 'Generic SIM access (raw APDU to SIM)' },
{ cmd: 'AT+CLCK', desc: 'Facility lock (PIN enable/disable, network lock query)' },
{ cmd: 'AT+CPWD', desc: 'Change password (PIN/PIN2/PUK)' },
],
'Modem Internals & Debug': [
{ cmd: 'AT+CGMI', desc: 'Manufacturer: Samsung Electronics' },
{ cmd: 'AT+CGMM', desc: 'Model: S5400BUNUELO (Shannon 5400)' },
{ cmd: 'AT+CGMR', desc: 'Firmware: g5400i-251201-260127-B-14784805' },
{ cmd: 'AT+GCAP', desc: 'Modem capabilities (+CGSM)' },
{ cmd: 'AT$ARME', desc: 'ARM Engine — low-level modem subsystem control' },
{ cmd: 'AT$ARMEE', desc: 'ARM Engine extended — deeper subsystem access' },
{ cmd: 'AT+DEVFEAT', desc: 'Device feature flags — query supported hardware features' },
{ cmd: 'AT+CEER', desc: 'Extended error report — last call/data failure cause code' },
{ cmd: 'AT+CMEE=2', desc: 'Enable verbose error messages (CME ERROR text)' },
{ cmd: 'AT+CLAC', desc: 'List ALL supported AT commands' },
{ cmd: 'AT+PACSP?', desc: 'Preferred ACSP setting' },
],
'Satellite / NTN': [
{ cmd: 'AT+SKYLOTEST', desc: 'Satellite IoT (Skylo/NTN) test mode — non-terrestrial network testing' },
],
'Carrier-Specific (Hidden)': [
{ cmd: 'AT+VZWRSRP', desc: 'Verizon RSRP — carrier-specific signal measurement' },
{ cmd: 'AT+VZWRSRQ', desc: 'Verizon RSRQ — carrier-specific signal quality' },
{ cmd: 'AT+VZWAPNE?', desc: 'Verizon APN entries — hidden APN config' },
{ cmd: 'AT+VZWMRUC', desc: 'Verizon most recently used channel' },
{ cmd: 'AT+VZWMRUE', desc: 'Verizon most recently used EARFCN' },
{ cmd: 'AT+VZWRPLMNC', desc: 'Verizon roaming PLMN config' },
{ cmd: 'AT+SKT', desc: 'SK Telecom specific query/config' },
],
'SMS & Call': [
{ cmd: 'AT+CMGF?', desc: 'SMS message format (0=PDU, 1=text)' },
{ cmd: 'AT+CMGL="ALL"', desc: 'List SMS messages' },
{ cmd: 'AT+CNMI?', desc: 'New message indication settings' },
{ cmd: 'AT+CSCA?', desc: 'SMS service center address' },
{ cmd: 'AT+CGSMS?', desc: 'Select service for MO SMS (PS vs CS)' },
{ cmd: 'AT+CNMPSD', desc: 'Non-MO PS data indication' },
{ cmd: 'AT+CLCC', desc: 'List current calls' },
{ cmd: 'AT+CHLD=?', desc: 'Call hold/multiparty capabilities' },
{ cmd: 'AT+CLIP?', desc: 'Caller ID presentation status' },
{ cmd: 'AT+CLIR?', desc: 'Caller ID restriction status' },
{ cmd: 'AT+CCWA?', desc: 'Call waiting status' },
{ cmd: 'AT+CCFC=?', desc: 'Call forwarding capabilities' },
{ cmd: 'AT+CUSD=?', desc: 'USSD capabilities' },
],
};
async function loadTerminal() {
config = await api('/api/status') || config;
const soc = config.detected_soc || 'unknown';
// Render quick presets
const presets = [...AT_PRESETS.common, ...(AT_PRESETS[soc] || [])];
const container = document.getElementById('at-presets');
document.getElementById('at-presets-title').textContent =
`Quick Commands — ${soc.toUpperCase()}`;
container.innerHTML = presets.map(p => `
<div class="card-row" style="cursor:pointer" onclick="runPreset('${p.cmd.replace(/'/g, "\\'")}')">
<div class="card-row-info">
<span class="prop-name" style="color:var(--accent)">${escHtml(p.cmd)}</span>
<div class="card-row-desc">${p.desc}</div>
</div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--text-muted)"><path d="M8 5v14l11-7z"/></svg>
</div>
`).join('');
// Render discovered commands reference
renderDiscoveredCommands();
renderATHistory();
}
function runPreset(cmd) {
document.getElementById('at-input').value = cmd;
sendATCmd();
}
async function sendATCmd() {
const input = document.getElementById('at-input');
const cmd = input.value.trim();
if (!cmd) return;
atHistory.push({ type: 'cmd', text: cmd, time: new Date().toLocaleTimeString() });
renderATHistory();
input.value = '';
const res = await post('/api/at', { cmd, timeout: '5' });
if (res && res.response) {
atHistory.push({ type: 'resp', text: res.response, time: new Date().toLocaleTimeString() });
} else {
atHistory.push({ type: 'err', text: 'No response / timeout', time: new Date().toLocaleTimeString() });
}
renderATHistory();
}
function renderATHistory() {
const el = document.getElementById('at-history');
if (atHistory.length === 0) {
el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:8px">No commands sent yet. Type an AT command or tap a preset below.</div>';
return;
}
el.innerHTML = atHistory.map(h => {
if (h.type === 'cmd') return `<div class="term-cmd"><span class="term-prompt">&gt;</span> ${escHtml(h.text)} <span class="term-time">${h.time}</span></div>`;
if (h.type === 'err') return `<div class="term-err">${escHtml(h.text)}</div>`;
return `<div class="term-resp">${escHtml(h.text)}</div>`;
}).join('');
el.scrollTop = el.scrollHeight;
}
function renderDiscoveredCommands() {
const container = document.getElementById('discovered-commands');
if (!container) return;
container.innerHTML = Object.entries(DISCOVERED_COMMANDS).map(([category, cmds]) => `
<div class="card" style="margin-bottom:8px">
<div class="card-row" style="cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'block':'none'">
<div class="card-row-info">
<div class="card-row-label">${category}</div>
<div class="card-row-desc">${cmds.length} commands</div>
</div>
<span style="color:var(--text-muted);font-size:12px">tap to expand</span>
</div>
<div style="display:none">
${cmds.map(c => `
<div class="card-row" style="cursor:pointer" onclick="runPreset('${c.cmd.replace(/'/g, "\\'")}')">
<div class="card-row-info">
<span class="prop-name" style="color:var(--accent)">${escHtml(c.cmd)}</span>
<div class="card-row-desc">${c.desc}</div>
</div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--text-muted)"><path d="M8 5v14l11-7z"/></svg>
</div>
`).join('')}
</div>
</div>
`).join('');
}
// ---- WiFi page ----
async function loadWifi() {
config = await api('/api/status') || config;
// Kmod status
const kmod = await api('/api/kmod');
if (kmod) {
updateKmodBtn('btn-kmod-wifi', kmod.wifi_mon);
updateKmodBtn('btn-kmod-shannon', kmod.shannon_cmd);
updateKmodBtn('btn-kmod-diag', kmod.diag_bridge);
setChip('chip-kmod', kmod.wifi_mon || kmod.shannon_cmd || kmod.diag_bridge);
}
// WiFi mode buttons
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === (config.wifi_mode || 'managed'));
});
// BCM4390 driver parameters
const params = await api('/api/wifi/params');
const pc = document.getElementById('wifi-params');
if (params && params.length > 0) {
pc.innerHTML = params.map(p => `
<div class="card-row">
<div class="card-row-info">
<span class="prop-name">${p.name}</span>
<div class="card-row-desc" style="word-break:break-all">${escHtml(p.value) || '(empty)'}</div>
</div>
${p.writable ? `<button class="btn" style="padding:4px 8px;font-size:11px;flex-shrink:0" onclick="editWifiParam('${p.name}','${escHtml(p.value)}')">Edit</button>` : ''}
</div>`).join('');
}
// WiFi firmware files
const fw = await api('/api/wifi/firmware');
const fwc = document.getElementById('wifi-firmware');
if (fw) {
let html = '';
if (fw.info) {
html += `<div class="card-row"><div class="card-row-info">
<div class="card-row-label">Active Firmware</div>
<div class="card-row-desc" style="white-space:pre-wrap;font-family:monospace;font-size:11px">${escHtml(fw.info)}</div>
</div></div>`;
}
if (fw.files && fw.files.length > 0) {
html += fw.files.map(f => `
<div class="card-row">
<div class="card-row-info">
<span class="prop-name">${f.name}</span>
<div class="card-row-desc">${(f.size / 1024).toFixed(0)} KB — ${f.path}</div>
</div>
</div>`).join('');
}
fwc.innerHTML = html || '<div class="card-row"><div class="card-row-desc">No firmware files found</div></div>';
}
// WiFi hardware info
const wifiText = await fetch('/api/wifi/info').then(r => r.text()).catch(() => '');
const wc = document.getElementById('wifi-hw-info');
if (wifiText.trim()) {
const lines = wifiText.trim().split('\n').filter(l => l.includes('='));
wc.innerHTML = lines.map(l => {
const [k, ...v] = l.split('=');
return `<div class="card-row">
<div class="card-row-info">
<span class="prop-name">${k}</span>
<div class="card-row-desc">${v.join('=')}</div>
</div>
</div>`;
}).join('');
}
}
async function editWifiParam(name, currentVal) {
const newVal = prompt(`Set ${name}:`, currentVal);
if (newVal === null) return;
const res = await post('/api/wifi/param', { name, value: newVal });
if (res && res.ok) {
showToast(`${name} = ${res.value}`);
loadWifi();
} else {
showToast(res?.error || 'Failed to set param', 'error');
}
}
function updateKmodBtn(id, loaded) {
const btn = document.getElementById(id);
if (!btn) return;
if (loaded) {
btn.textContent = 'Unload';
btn.className = 'btn btn-danger';
btn.style.cssText = 'padding:6px 12px;font-size:12px';
} else {
btn.textContent = 'Load';
btn.className = 'btn btn-primary';
btn.style.cssText = 'padding:6px 12px;font-size:12px';
}
}
async function toggleKmod(mod) {
const kmod = await api('/api/kmod');
const key = mod.replace('rc_', '').replace('_cmd', '_cmd');
const mapKey = mod === 'rc_wifi_mon' ? 'wifi_mon' : mod === 'rc_shannon_cmd' ? 'shannon_cmd' : 'diag_bridge';
const loaded = kmod && kmod[mapKey];
const action = loaded ? 'unload' : 'load';
const res = await post(`/api/kmod/${action}`, { module: mod });
if (res) {
showToast(res.ok ? `${mod}: ${res.status}` : (res.error || 'Failed'), res.ok ? 'success' : 'error');
}
loadWifi();
}
async function handleWifiMode(mode) {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
document.querySelector(`[data-mode="${mode}"]`).classList.add('active');
const res = await post('/api/wifi/mode', { mode });
if (res && res.ok) {
config.wifi_mode = mode;
updateChips();
showToast(`WiFi: ${mode} on ${res.iface}`);
} else {
showToast(res?.error || 'Failed — load rc_wifi_mon first?', 'error');
loadWifi();
}
}
// ---- debugfs browser ----
async function loadDebug() {
const paths = await api('/api/debugfs');
const sc = document.getElementById('debugfs-shortcuts');
if (paths && paths.length > 0) {
sc.innerHTML = paths.map(p => `
<div class="card-row" style="cursor:pointer" onclick="browseToPath('${p}')">
<div class="card-row-info">
<span class="prop-name">${p.replace('/sys/kernel/debug/', '/d/')}</span>
</div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--text-muted)"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</div>
`).join('');
}
}
function browseToPath(path) {
document.getElementById('fs-path').value = path;
browsePath();
}
async function browsePath() {
const path = document.getElementById('fs-path').value.trim();
if (!path) return;
const res = await post('/api/fs/read', { path });
const fc = document.getElementById('fs-content');
if (!res) {
fc.innerHTML = '<div class="card-row"><div class="card-row-desc">Error reading path</div></div>';
return;
}
// Breadcrumb
const parts = path.split('/').filter(Boolean);
document.getElementById('fs-breadcrumb').innerHTML =
parts.map((p, i) => {
const fullPath = '/' + parts.slice(0, i + 1).join('/');
return `<span class="crumb" onclick="browseToPath('${fullPath}')">${p}</span>`;
}).join(' / ');
if (res.type === 'dir' && res.entries) {
fc.innerHTML = res.entries.map(e => {
const icon = e.type === 'dir' ? '&#128193;' : (e.writable ? '&#128221;' : '&#128196;');
return `<div class="card-row" style="cursor:pointer" onclick="${e.type === 'dir' ? `browseToPath('${path}/${e.name}')` : `readFile('${path}/${e.name}')`}">
<div class="card-row-info">
<div class="card-row-label">${icon} ${e.name}</div>
<div class="card-row-desc">${e.type}${e.writable ? ' (rw)' : ' (ro)'}</div>
</div>
</div>`;
}).join('') || '<div class="card-row"><div class="card-row-desc">Empty directory</div></div>';
} else if (res.type === 'file') {
const content = res.content || '(empty)';
fc.innerHTML = `<div style="padding:12px 16px">
<div class="section-title" style="margin-bottom:8px">${path.split('/').pop()}</div>
<pre class="file-content">${escHtml(content)}</pre>
</div>`;
}
}
function readFile(path) {
document.getElementById('fs-path').value = path;
browsePath();
}
function fsUp() {
const input = document.getElementById('fs-path');
const parts = input.value.split('/').filter(Boolean);
if (parts.length > 1) {
parts.pop();
input.value = '/' + parts.join('/');
browsePath();
}
}
// ---- Carrier page ----
async function loadCarrier() {
const cc = await api('/api/carrier/config');
if (cc) {
setToggle('toggle-volte', cc.volte);
setToggle('toggle-wfc', cc.wfc);
setToggle('toggle-nettype', cc.hide_network_type === '0' ? '1' : '0');
}
// Carrier settings files
const files = await api('/api/carrier/files');
const fc = document.getElementById('carrier-files');
if (files && files.length > 0) {
fc.innerHTML = files.map(f => `
<div class="card-row">
<div class="card-row-info">
<span class="prop-name">${f.name}</span>
<div class="card-row-desc">${(f.size / 1024).toFixed(1)} KB</div>
</div>
</div>`).join('');
} else {
fc.innerHTML = '<div class="card-row"><div class="card-row-desc">No CarrierSettings directory found</div></div>';
}
}
async function handleCarrierFlag(flag, el) {
const val = el.checked ? '1' : '0';
const res = await post('/api/carrier/set', { flag, value: val });
if (res && res.ok) {
showToast(`${flag} = ${val}`);
} else {
el.checked = !el.checked;
showToast(res?.error || 'Failed', 'error');
}
}
async function dumpCarrierConfig() {
const pre = document.getElementById('carrier-dump');
pre.style.display = 'block';
pre.textContent = 'Loading...';
const res = await api('/api/carrier/dump');
if (res && res.dump) {
pre.textContent = res.dump;
} else {
pre.textContent = 'Failed to dump carrier_config';
}
}
// ---- CP Debug (in Radio page) ----
async function loadCPDebug() {
const cp = await api('/api/cp');
if (cp) {
setText('cp-state', cp.state || 'unknown');
document.getElementById('cp-pcie').textContent = cp.pcie_stats ? cp.pcie_stats.substring(0, 200) : 'N/A';
document.getElementById('cp-sbb').textContent = cp.sbb_debug ? cp.sbb_debug.substring(0, 200) : 'N/A';
}
}
async function triggerCPCrash() {
if (!confirm('This will crash the modem and generate a ramdump.\nThe modem will restart automatically.\n\nContinue?')) return;
const res = await post('/api/cp/crash', {});
if (res && res.ok) {
showToast(res.msg || 'CP crash triggered');
} else {
showToast(res?.error || 'Failed', 'error');
}
}
// ---- Thread / Wonder radio (in Debug page) ----
async function loadThreadInfo() {
const info = await api('/api/thread');
const tc = document.getElementById('thread-info');
if (info && info.present) {
tc.innerHTML = `
<div class="card-row"><div class="card-row-info">
<div class="card-row-label">Wonder 802.15.4 Radio</div>
<div class="card-row-desc">Thread / Matter mesh networking</div>
</div></div>
<div class="card-row"><div class="card-row-info">
<span class="prop-name">MAC</span>
<div class="card-row-desc">${info.mac || 'N/A'}</div>
</div></div>
<div class="card-row"><div class="card-row-info">
<span class="prop-name">PHY</span>
<div class="card-row-desc">${info.name || 'N/A'} (index ${info.index || '?'})</div>
</div></div>
<div class="card-row"><div class="card-row-info">
<span class="prop-name">WPAN Interface</span>
<div class="card-row-desc">${info.wpan_iface ? 'thread-wpan (active)' : 'not present'}</div>
</div></div>
${info.debugfs_entries ? `<div class="card-row"><div class="card-row-info">
<span class="prop-name">debugfs nodes</span>
<div class="card-row-desc">${info.debugfs_entries}</div>
</div></div>` : ''}
${info.force_stop_tx !== undefined ? `<div class="card-row"><div class="card-row-info">
<span class="prop-name">force_stop_tx</span>
<div class="card-row-desc">${info.force_stop_tx}</div>
</div></div>` : ''}
`;
} else {
tc.innerHTML = '<div class="card-row"><div class="card-row-desc">No Thread/Wonder radio detected</div></div>';
}
}
// ---- Flags page ----
async function loadFlags() {
const flags = await api('/api/flags');
const tbody = document.getElementById('flags-body');
if (flags && flags.length > 0) {
tbody.innerHTML = flags.map(f => `<tr>
<td class="prop-name">${f.prop}</td>
<td class="prop-value">${f.value || '<span style="color:var(--text-muted)">unset</span>'}</td>
</tr>`).join('');
}
}
// ---- Helpers ----
function escHtml(s) {
return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ---- Init ----
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.tab-item').forEach(t =>
t.addEventListener('click', () => switchTab(t.dataset.tab)));
// Radio page toggles
document.getElementById('toggle-eng')?.addEventListener('change', function() { handleToggle('ENGINEERING_MODE', this); });
document.getElementById('toggle-factory')?.addEventListener('change', function() { handleToggle('FACTORY_TEST_MODE', this); });
document.getElementById('toggle-hidden')?.addEventListener('change', function() { handleToggle('HIDDEN_MENUS', this); });
document.getElementById('toggle-diag')?.addEventListener('change', function() { handleToggle('USB_DIAG_MODE', this); });
document.getElementById('toggle-modem')?.addEventListener('change', function() { handleToggle('MODEM_LOG', this); });
// WiFi mode buttons
document.querySelectorAll('.mode-btn').forEach(btn =>
btn.addEventListener('click', () => handleWifiMode(btn.dataset.mode)));
// Carrier config toggles
document.getElementById('toggle-volte')?.addEventListener('change', function() { handleCarrierFlag('volte', this); });
document.getElementById('toggle-vonr')?.addEventListener('change', function() { handleCarrierFlag('vonr', this); });
document.getElementById('toggle-wfc')?.addEventListener('change', function() { handleCarrierFlag('wfc', this); });
document.getElementById('toggle-vt')?.addEventListener('change', function() { handleCarrierFlag('vt', this); });
document.getElementById('toggle-apn')?.addEventListener('change', function() { handleCarrierFlag('apn', this); });
document.getElementById('toggle-nradv')?.addEventListener('change', function() { handleCarrierFlag('nradv', this); });
document.getElementById('toggle-nettype')?.addEventListener('change', function() { handleCarrierFlag('nettype', this); });
switchTab('radio');
});