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:
62
META-INF/com/google/android/update-binary
Executable file
62
META-INF/com/google/android/update-binary
Executable 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"
|
||||||
1
META-INF/com/google/android/updater-script
Normal file
1
META-INF/com/google/android/updater-script
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#MAGISK
|
||||||
22
common/kmod/Makefile
Normal file
22
common/kmod/Makefile
Normal 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
54
common/kmod/README.md
Normal 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.
|
||||||
398
common/kmod/rc_diag_bridge.c
Normal file
398
common/kmod/rc_diag_bridge.c
Normal 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);
|
||||||
306
common/kmod/rc_shannon_cmd.c
Normal file
306
common/kmod/rc_shannon_cmd.c
Normal 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
340
common/kmod/rc_wifi_mon.c
Normal 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
6
module.prop
Normal 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
159
post-fs-data.sh
Executable 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
|
||||||
26
sepolicy/radiocontrol.rule
Normal file
26
sepolicy/radiocontrol.rule
Normal 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
242
service.sh
Executable 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
726
system/bin/radiocontrol
Executable 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
23
uninstall.sh
Executable 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
574
webroot/css/style.css
Normal 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
386
webroot/index.html
Normal 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
828
webroot/js/app.js
Normal 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}°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">></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' ? '📁' : (e.writable ? '📝' : '📄');
|
||||||
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user