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

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

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

View File

@@ -0,0 +1,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);