v1.1.0: Complete kernel modules, fix WebUI bugs
Kernel modules fully implemented for kernel 6.6/Tensor G5: - rc_wifi_mon: kprobes kallsyms, bcmdhd iovar monitor/promisc/allmulti, sysfs status at /sys/kernel/rc_wifi_mon/, clean unpatch on unload - rc_shannon_cmd: ioctl interface (AT_CMD, GET_URC, SET_TIMEOUT, GET_STATUS, FLUSH), URC ring buffer (64 entries), modem probe on init - rc_diag_bridge: HDLC decode with CRC-16 validation, FTM ioctl, EFS read/write/stat/unlink, version query, subsystem dispatch - rc_ioctl.h: shared userspace header for all ioctl definitions - All modules handle class_create() API change in kernel 6.4+ WebUI fixes: - Fix malformed WiFi firmware JSON output - Add vonr/vt/apn/nradv to carrier config read endpoint - Fix carrier toggle state loading in frontend - Fix redundant replace in kmod toggle logic Makefile: single-module build (MOD=), make package target uninstall.sh: unload kernel modules before cleanup
This commit is contained in:
@@ -6,20 +6,23 @@
|
||||
* 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
|
||||
* On Tensor G5: Shannon 5400 (S5400BUNUELO), paths include
|
||||
* /dev/umts_router, /dev/umts_atc0, /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
|
||||
* 4. Handles URCs (unsolicited result codes) from the modem
|
||||
* 5. 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
|
||||
*
|
||||
* Target: Pixel 10 Pro Fold (rango), Tensor G5, kernel 6.6
|
||||
*/
|
||||
|
||||
#include <linux/module.h>
|
||||
@@ -35,6 +38,9 @@
|
||||
#include <linux/poll.h>
|
||||
#include <linux/kthread.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/ioctl.h>
|
||||
#include <linux/circ_buf.h>
|
||||
#include <linux/version.h>
|
||||
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_AUTHOR("RadioControl");
|
||||
@@ -43,14 +49,54 @@ MODULE_VERSION("1.0");
|
||||
|
||||
#define DEVICE_NAME "rc_shannon"
|
||||
#define CLASS_NAME "radiocontrol"
|
||||
#define BUF_SIZE 4096
|
||||
#define CMD_BUF_SIZE 4096
|
||||
#define RESP_BUF_SIZE 8192
|
||||
#define URC_BUF_SIZE 16384
|
||||
#define MAX_CLIENTS 4
|
||||
|
||||
/* IOCTL commands */
|
||||
#define RC_SHANNON_MAGIC 'S'
|
||||
#define RC_SHANNON_AT_CMD _IOWR(RC_SHANNON_MAGIC, 1, struct rc_at_cmd)
|
||||
#define RC_SHANNON_GET_URC _IOR(RC_SHANNON_MAGIC, 2, struct rc_urc_msg)
|
||||
#define RC_SHANNON_SET_TIMEOUT _IOW(RC_SHANNON_MAGIC, 3, int)
|
||||
#define RC_SHANNON_GET_STATUS _IOR(RC_SHANNON_MAGIC, 4, struct rc_modem_status)
|
||||
#define RC_SHANNON_FLUSH _IO(RC_SHANNON_MAGIC, 5)
|
||||
|
||||
/* AT command with explicit timeout */
|
||||
struct rc_at_cmd {
|
||||
char cmd[CMD_BUF_SIZE];
|
||||
uint32_t cmd_len;
|
||||
char resp[RESP_BUF_SIZE];
|
||||
uint32_t resp_len;
|
||||
uint32_t timeout_ms; /* 0 = use default */
|
||||
int32_t status; /* 0=OK, -1=ERROR, -2=TIMEOUT, -3=CME ERROR */
|
||||
};
|
||||
|
||||
/* Unsolicited result code */
|
||||
struct rc_urc_msg {
|
||||
char data[1024];
|
||||
uint32_t data_len;
|
||||
int32_t remaining; /* URCs still queued */
|
||||
};
|
||||
|
||||
/* Modem status info */
|
||||
struct rc_modem_status {
|
||||
char device_path[128];
|
||||
int32_t connected;
|
||||
int32_t urc_count;
|
||||
uint64_t cmds_sent;
|
||||
uint64_t cmds_failed;
|
||||
uint64_t bytes_tx;
|
||||
uint64_t bytes_rx;
|
||||
};
|
||||
|
||||
/* 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 */
|
||||
"/dev/umts_router", /* Tensor primary (no trailing 0) */
|
||||
"/dev/umts_router0", /* Exynos primary — last resort */
|
||||
NULL
|
||||
};
|
||||
|
||||
@@ -59,14 +105,124 @@ static struct class *rc_class;
|
||||
static struct cdev rc_cdev;
|
||||
static struct device *rc_device;
|
||||
static struct file *modem_filp;
|
||||
static char modem_path_used[128];
|
||||
static DEFINE_MUTEX(cmd_mutex);
|
||||
static DECLARE_WAIT_QUEUE_HEAD(resp_waitq);
|
||||
static DECLARE_WAIT_QUEUE_HEAD(urc_waitq);
|
||||
|
||||
/* Response buffer */
|
||||
static char resp_buf[BUF_SIZE];
|
||||
/* Response buffer for synchronous command/response */
|
||||
static char resp_buf[RESP_BUF_SIZE];
|
||||
static int resp_len;
|
||||
static bool resp_ready;
|
||||
|
||||
/* URC circular buffer */
|
||||
struct urc_entry {
|
||||
char data[1024];
|
||||
int len;
|
||||
};
|
||||
static struct urc_entry urc_ring[64];
|
||||
static int urc_head;
|
||||
static int urc_tail;
|
||||
static DEFINE_SPINLOCK(urc_lock);
|
||||
|
||||
/* Statistics */
|
||||
static uint64_t stat_cmds_sent;
|
||||
static uint64_t stat_cmds_failed;
|
||||
static uint64_t stat_bytes_tx;
|
||||
static uint64_t stat_bytes_rx;
|
||||
|
||||
/* Reader thread */
|
||||
static struct task_struct *reader_thread;
|
||||
static int default_timeout_ms = 5000;
|
||||
|
||||
/* Common URC prefixes from Shannon modems */
|
||||
static const char *urc_prefixes[] = {
|
||||
"+CRING:", "+CLIP:", "+CREG:", "+CGREG:",
|
||||
"+CEREG:", "+C5GREG:", "+CMTI:", "+CMT:",
|
||||
"+CDS:", "+CUSD:", "+CCWA:", "+CSSI:",
|
||||
"+CSSU:", "+COPS:", "RING", "NO CARRIER",
|
||||
"+CGEV:", "+CIEV:", "+AIMS", "$",
|
||||
NULL
|
||||
};
|
||||
|
||||
static bool is_urc(const char *line)
|
||||
{
|
||||
int i;
|
||||
|
||||
/* Skip leading \r\n */
|
||||
while (*line == '\r' || *line == '\n')
|
||||
line++;
|
||||
|
||||
for (i = 0; urc_prefixes[i]; i++) {
|
||||
if (strncmp(line, urc_prefixes[i],
|
||||
strlen(urc_prefixes[i])) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void urc_enqueue(const char *data, int len)
|
||||
{
|
||||
unsigned long flags;
|
||||
int next;
|
||||
|
||||
spin_lock_irqsave(&urc_lock, flags);
|
||||
next = (urc_head + 1) % ARRAY_SIZE(urc_ring);
|
||||
if (next == urc_tail) {
|
||||
/* Ring full — drop oldest */
|
||||
urc_tail = (urc_tail + 1) % ARRAY_SIZE(urc_ring);
|
||||
}
|
||||
if (len > sizeof(urc_ring[0].data) - 1)
|
||||
len = sizeof(urc_ring[0].data) - 1;
|
||||
memcpy(urc_ring[urc_head].data, data, len);
|
||||
urc_ring[urc_head].data[len] = '\0';
|
||||
urc_ring[urc_head].len = len;
|
||||
urc_head = next;
|
||||
spin_unlock_irqrestore(&urc_lock, flags);
|
||||
|
||||
wake_up_interruptible(&urc_waitq);
|
||||
}
|
||||
|
||||
static int urc_dequeue(struct rc_urc_msg *msg)
|
||||
{
|
||||
unsigned long flags;
|
||||
int count;
|
||||
|
||||
spin_lock_irqsave(&urc_lock, flags);
|
||||
if (urc_head == urc_tail) {
|
||||
spin_unlock_irqrestore(&urc_lock, flags);
|
||||
return -EAGAIN;
|
||||
}
|
||||
msg->data_len = urc_ring[urc_tail].len;
|
||||
memcpy(msg->data, urc_ring[urc_tail].data, msg->data_len);
|
||||
msg->data[msg->data_len] = '\0';
|
||||
urc_tail = (urc_tail + 1) % ARRAY_SIZE(urc_ring);
|
||||
|
||||
/* Count remaining */
|
||||
if (urc_head >= urc_tail)
|
||||
count = urc_head - urc_tail;
|
||||
else
|
||||
count = ARRAY_SIZE(urc_ring) - urc_tail + urc_head;
|
||||
msg->remaining = count;
|
||||
spin_unlock_irqrestore(&urc_lock, flags);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int urc_count(void)
|
||||
{
|
||||
unsigned long flags;
|
||||
int count;
|
||||
|
||||
spin_lock_irqsave(&urc_lock, flags);
|
||||
if (urc_head >= urc_tail)
|
||||
count = urc_head - urc_tail;
|
||||
else
|
||||
count = ARRAY_SIZE(urc_ring) - urc_tail + urc_head;
|
||||
spin_unlock_irqrestore(&urc_lock, flags);
|
||||
return count;
|
||||
}
|
||||
|
||||
/*
|
||||
* Open the underlying modem device from kernel context.
|
||||
*/
|
||||
@@ -78,6 +234,8 @@ static struct file *open_modem_device(void)
|
||||
for (i = 0; modem_paths[i]; i++) {
|
||||
f = filp_open(modem_paths[i], O_RDWR | O_NONBLOCK, 0);
|
||||
if (!IS_ERR(f)) {
|
||||
strscpy(modem_path_used, modem_paths[i],
|
||||
sizeof(modem_path_used));
|
||||
pr_info("rc_shannon: opened modem device: %s\n",
|
||||
modem_paths[i]);
|
||||
return f;
|
||||
@@ -91,46 +249,104 @@ static struct file *open_modem_device(void)
|
||||
|
||||
/*
|
||||
* Send an AT command to the modem and read the response.
|
||||
* Separates URCs from command responses.
|
||||
*/
|
||||
static int send_at_command(const char *cmd, int cmd_len,
|
||||
char *response, int resp_size)
|
||||
char *response, int resp_size, int timeout_ms)
|
||||
{
|
||||
loff_t pos = 0;
|
||||
ssize_t written, bytes_read;
|
||||
int timeout_ms = 3000;
|
||||
int elapsed = 0;
|
||||
int total_read = 0;
|
||||
char line_buf[1024];
|
||||
int line_pos = 0;
|
||||
bool in_response = false;
|
||||
|
||||
if (!modem_filp || IS_ERR(modem_filp))
|
||||
return -ENODEV;
|
||||
|
||||
if (timeout_ms <= 0)
|
||||
timeout_ms = default_timeout_ms;
|
||||
|
||||
/* 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);
|
||||
stat_cmds_failed++;
|
||||
return written;
|
||||
}
|
||||
stat_bytes_tx += written;
|
||||
stat_cmds_sent++;
|
||||
|
||||
/* Read response with timeout */
|
||||
/* Read response with timeout, filtering URCs */
|
||||
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);
|
||||
char tmp[512];
|
||||
|
||||
bytes_read = kernel_read(modem_filp, tmp, sizeof(tmp), &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;
|
||||
int i;
|
||||
|
||||
stat_bytes_rx += bytes_read;
|
||||
|
||||
for (i = 0; i < bytes_read; i++) {
|
||||
char c = tmp[i];
|
||||
|
||||
/* Build lines to check for URCs */
|
||||
if (c == '\n' || line_pos >= sizeof(line_buf) - 1) {
|
||||
line_buf[line_pos] = '\0';
|
||||
|
||||
if (line_pos > 0 && is_urc(line_buf)) {
|
||||
/* It's a URC — queue it, don't add to response */
|
||||
urc_enqueue(line_buf, line_pos);
|
||||
} else {
|
||||
/* Part of command response */
|
||||
if (total_read + line_pos + 1 < resp_size) {
|
||||
memcpy(response + total_read,
|
||||
line_buf, line_pos);
|
||||
total_read += line_pos;
|
||||
response[total_read++] = '\n';
|
||||
in_response = true;
|
||||
}
|
||||
}
|
||||
line_pos = 0;
|
||||
} else if (c != '\r') {
|
||||
line_buf[line_pos++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for final response in accumulated data */
|
||||
if (in_response) {
|
||||
if (strnstr(response, "OK", total_read) ||
|
||||
strnstr(response, "ERROR", total_read) ||
|
||||
strnstr(response, "+CME ERROR:", total_read) ||
|
||||
strnstr(response, "+CMS ERROR:", total_read))
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
msleep(50);
|
||||
elapsed += 50;
|
||||
msleep(20);
|
||||
elapsed += 20;
|
||||
}
|
||||
}
|
||||
|
||||
/* Flush any remaining partial line */
|
||||
if (line_pos > 0) {
|
||||
line_buf[line_pos] = '\0';
|
||||
if (is_urc(line_buf)) {
|
||||
urc_enqueue(line_buf, line_pos);
|
||||
} else if (total_read + line_pos < resp_size) {
|
||||
memcpy(response + total_read, line_buf, line_pos);
|
||||
total_read += line_pos;
|
||||
}
|
||||
}
|
||||
|
||||
response[total_read] = '\0';
|
||||
|
||||
if (elapsed >= timeout_ms && total_read == 0)
|
||||
return -ETIMEDOUT;
|
||||
|
||||
return total_read;
|
||||
}
|
||||
|
||||
@@ -147,13 +363,16 @@ static int rc_release(struct inode *inode, struct file *file)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* write() — send AT command, response available via read()
|
||||
*/
|
||||
static ssize_t rc_write(struct file *file, const char __user *buf,
|
||||
size_t count, loff_t *ppos)
|
||||
{
|
||||
char cmd_buf[BUF_SIZE];
|
||||
char cmd_buf[CMD_BUF_SIZE];
|
||||
int ret;
|
||||
|
||||
if (count >= BUF_SIZE)
|
||||
if (count >= CMD_BUF_SIZE - 2)
|
||||
return -EINVAL;
|
||||
|
||||
if (copy_from_user(cmd_buf, buf, count))
|
||||
@@ -162,12 +381,17 @@ static ssize_t rc_write(struct file *file, const char __user *buf,
|
||||
|
||||
mutex_lock(&cmd_mutex);
|
||||
|
||||
/* Ensure command ends with \r\n */
|
||||
/* Ensure command ends with \r\n for the Shannon modem */
|
||||
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 if (count >= 1 && cmd_buf[count-1] == '\n') {
|
||||
/* Shift to insert \r before \n */
|
||||
cmd_buf[count] = cmd_buf[count-1];
|
||||
cmd_buf[count-1] = '\r';
|
||||
count++;
|
||||
} else {
|
||||
cmd_buf[count] = '\r';
|
||||
cmd_buf[count+1] = '\n';
|
||||
@@ -175,18 +399,25 @@ static ssize_t rc_write(struct file *file, const char __user *buf,
|
||||
}
|
||||
cmd_buf[count] = '\0';
|
||||
|
||||
ret = send_at_command(cmd_buf, count, resp_buf, BUF_SIZE);
|
||||
ret = send_at_command(cmd_buf, count, resp_buf, RESP_BUF_SIZE,
|
||||
default_timeout_ms);
|
||||
if (ret >= 0) {
|
||||
resp_len = ret;
|
||||
resp_ready = true;
|
||||
wake_up_interruptible(&resp_waitq);
|
||||
} else {
|
||||
resp_len = 0;
|
||||
resp_ready = false;
|
||||
}
|
||||
|
||||
mutex_unlock(&cmd_mutex);
|
||||
|
||||
return ret >= 0 ? count : ret;
|
||||
return ret >= 0 ? (ssize_t)count : ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* read() — get the response from the last AT command
|
||||
*/
|
||||
static ssize_t rc_read(struct file *file, char __user *buf,
|
||||
size_t count, loff_t *ppos)
|
||||
{
|
||||
@@ -195,7 +426,8 @@ static ssize_t rc_read(struct file *file, char __user *buf,
|
||||
if (!resp_ready) {
|
||||
if (file->f_flags & O_NONBLOCK)
|
||||
return -EAGAIN;
|
||||
wait_event_interruptible(resp_waitq, resp_ready);
|
||||
if (wait_event_interruptible(resp_waitq, resp_ready))
|
||||
return -ERESTARTSYS;
|
||||
}
|
||||
|
||||
if (!resp_ready)
|
||||
@@ -209,24 +441,178 @@ static ssize_t rc_read(struct file *file, char __user *buf,
|
||||
return to_copy;
|
||||
}
|
||||
|
||||
static unsigned int rc_poll(struct file *file, poll_table *wait)
|
||||
static __poll_t rc_poll(struct file *file, poll_table *wait)
|
||||
{
|
||||
unsigned int mask = POLLOUT | POLLWRNORM;
|
||||
__poll_t mask = EPOLLOUT | EPOLLWRNORM;
|
||||
|
||||
poll_wait(file, &resp_waitq, wait);
|
||||
poll_wait(file, &urc_waitq, wait);
|
||||
|
||||
if (resp_ready)
|
||||
mask |= POLLIN | POLLRDNORM;
|
||||
mask |= EPOLLIN | EPOLLRDNORM;
|
||||
if (urc_count() > 0)
|
||||
mask |= EPOLLPRI; /* URCs available via ioctl */
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* ioctl — structured AT command interface
|
||||
*/
|
||||
static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
switch (cmd) {
|
||||
case RC_SHANNON_AT_CMD: {
|
||||
struct rc_at_cmd *at;
|
||||
|
||||
at = kmalloc(sizeof(*at), GFP_KERNEL);
|
||||
if (!at)
|
||||
return -ENOMEM;
|
||||
|
||||
if (copy_from_user(at, (void __user *)arg, sizeof(*at))) {
|
||||
kfree(at);
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
/* Sanity checks */
|
||||
if (at->cmd_len == 0 || at->cmd_len >= CMD_BUF_SIZE) {
|
||||
kfree(at);
|
||||
return -EINVAL;
|
||||
}
|
||||
at->cmd[at->cmd_len] = '\0';
|
||||
|
||||
/* Ensure \r\n termination */
|
||||
if (at->cmd_len < 2 ||
|
||||
at->cmd[at->cmd_len-2] != '\r' ||
|
||||
at->cmd[at->cmd_len-1] != '\n') {
|
||||
at->cmd[at->cmd_len++] = '\r';
|
||||
at->cmd[at->cmd_len++] = '\n';
|
||||
at->cmd[at->cmd_len] = '\0';
|
||||
}
|
||||
|
||||
mutex_lock(&cmd_mutex);
|
||||
ret = send_at_command(at->cmd, at->cmd_len,
|
||||
at->resp, sizeof(at->resp),
|
||||
at->timeout_ms ? at->timeout_ms :
|
||||
default_timeout_ms);
|
||||
mutex_unlock(&cmd_mutex);
|
||||
|
||||
if (ret >= 0) {
|
||||
at->resp_len = ret;
|
||||
/* Determine status from response */
|
||||
if (strnstr(at->resp, "OK", ret))
|
||||
at->status = 0;
|
||||
else if (strnstr(at->resp, "+CME ERROR:", ret))
|
||||
at->status = -3;
|
||||
else if (strnstr(at->resp, "+CMS ERROR:", ret))
|
||||
at->status = -3;
|
||||
else if (strnstr(at->resp, "ERROR", ret))
|
||||
at->status = -1;
|
||||
else
|
||||
at->status = 0; /* Got data but no final result */
|
||||
} else if (ret == -ETIMEDOUT) {
|
||||
at->resp_len = 0;
|
||||
at->status = -2;
|
||||
at->resp[0] = '\0';
|
||||
ret = 0; /* ioctl succeeded, timeout is in status */
|
||||
} else {
|
||||
at->status = ret;
|
||||
at->resp_len = 0;
|
||||
}
|
||||
|
||||
if (copy_to_user((void __user *)arg, at, sizeof(*at)))
|
||||
ret = -EFAULT;
|
||||
else
|
||||
ret = 0;
|
||||
|
||||
kfree(at);
|
||||
break;
|
||||
}
|
||||
|
||||
case RC_SHANNON_GET_URC: {
|
||||
struct rc_urc_msg msg;
|
||||
|
||||
ret = urc_dequeue(&msg);
|
||||
if (ret == -EAGAIN) {
|
||||
if (file->f_flags & O_NONBLOCK)
|
||||
return -EAGAIN;
|
||||
if (wait_event_interruptible(urc_waitq,
|
||||
urc_count() > 0))
|
||||
return -ERESTARTSYS;
|
||||
ret = urc_dequeue(&msg);
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (copy_to_user((void __user *)arg, &msg, sizeof(msg)))
|
||||
return -EFAULT;
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case RC_SHANNON_SET_TIMEOUT: {
|
||||
int timeout;
|
||||
|
||||
if (get_user(timeout, (int __user *)arg))
|
||||
return -EFAULT;
|
||||
if (timeout < 100 || timeout > 60000)
|
||||
return -EINVAL;
|
||||
default_timeout_ms = timeout;
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case RC_SHANNON_GET_STATUS: {
|
||||
struct rc_modem_status st;
|
||||
|
||||
memset(&st, 0, sizeof(st));
|
||||
strscpy(st.device_path, modem_path_used,
|
||||
sizeof(st.device_path));
|
||||
st.connected = (modem_filp && !IS_ERR(modem_filp)) ? 1 : 0;
|
||||
st.urc_count = urc_count();
|
||||
st.cmds_sent = stat_cmds_sent;
|
||||
st.cmds_failed = stat_cmds_failed;
|
||||
st.bytes_tx = stat_bytes_tx;
|
||||
st.bytes_rx = stat_bytes_rx;
|
||||
|
||||
if (copy_to_user((void __user *)arg, &st, sizeof(st)))
|
||||
return -EFAULT;
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case RC_SHANNON_FLUSH: {
|
||||
unsigned long flags;
|
||||
|
||||
spin_lock_irqsave(&urc_lock, flags);
|
||||
urc_head = 0;
|
||||
urc_tail = 0;
|
||||
spin_unlock_irqrestore(&urc_lock, flags);
|
||||
|
||||
resp_ready = false;
|
||||
resp_len = 0;
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
ret = -ENOTTY;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
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,
|
||||
.owner = THIS_MODULE,
|
||||
.open = rc_open,
|
||||
.release = rc_release,
|
||||
.read = rc_read,
|
||||
.write = rc_write,
|
||||
.poll = rc_poll,
|
||||
.unlocked_ioctl = rc_ioctl,
|
||||
.compat_ioctl = compat_ptr_ioctl,
|
||||
};
|
||||
|
||||
static int __init rc_shannon_init(void)
|
||||
@@ -242,8 +628,12 @@ static int __init rc_shannon_init(void)
|
||||
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 */
|
||||
/*
|
||||
* Don't fail — create the device node anyway so userspace
|
||||
* gets a clear -ENODEV on read/write rather than ENOENT
|
||||
* on open. The modem may come up later (e.g., after SIM
|
||||
* unlock or airplane mode toggle).
|
||||
*/
|
||||
}
|
||||
|
||||
/* Register char device */
|
||||
@@ -252,7 +642,16 @@ static int __init rc_shannon_init(void)
|
||||
goto err_chrdev;
|
||||
major = MAJOR(dev);
|
||||
|
||||
/*
|
||||
* class_create() signature changed in kernel 6.4:
|
||||
* 6.4+: class_create(name)
|
||||
* <6.4: class_create(THIS_MODULE, name)
|
||||
*/
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
|
||||
rc_class = class_create(CLASS_NAME);
|
||||
#else
|
||||
rc_class = class_create(THIS_MODULE, CLASS_NAME);
|
||||
#endif
|
||||
if (IS_ERR(rc_class)) {
|
||||
ret = PTR_ERR(rc_class);
|
||||
goto err_class;
|
||||
@@ -272,9 +671,27 @@ static int __init rc_shannon_init(void)
|
||||
goto err_device;
|
||||
}
|
||||
|
||||
/* Make device world-accessible (root context anyway) */
|
||||
pr_info("rc_shannon: /dev/%s created (major %d)\n",
|
||||
DEVICE_NAME, major);
|
||||
|
||||
if (modem_filp) {
|
||||
/* Verify modem is responsive */
|
||||
char test_resp[256];
|
||||
int test_ret;
|
||||
|
||||
mutex_lock(&cmd_mutex);
|
||||
test_ret = send_at_command("AT\r\n", 4, test_resp,
|
||||
sizeof(test_resp), 2000);
|
||||
mutex_unlock(&cmd_mutex);
|
||||
|
||||
if (test_ret > 0 && strnstr(test_resp, "OK", test_ret))
|
||||
pr_info("rc_shannon: modem responsive (AT -> OK)\n");
|
||||
else
|
||||
pr_warn("rc_shannon: modem opened but AT test "
|
||||
"failed (ret=%d) — may need SELinux permissive\n",
|
||||
test_ret);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
err_device:
|
||||
@@ -299,7 +716,9 @@ static void __exit rc_shannon_exit(void)
|
||||
if (modem_filp)
|
||||
filp_close(modem_filp, NULL);
|
||||
|
||||
pr_info("rc_shannon: unloaded\n");
|
||||
pr_info("rc_shannon: unloaded — sent %llu commands, "
|
||||
"%llu bytes tx, %llu bytes rx\n",
|
||||
stat_cmds_sent, stat_bytes_tx, stat_bytes_rx);
|
||||
}
|
||||
|
||||
module_init(rc_shannon_init);
|
||||
|
||||
Reference in New Issue
Block a user