blob: b358de03d00e59004d16ffe8aebacfac96f37305 [file] [log] [blame]
/*
* Copyright (C) 2025 metraTec GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT simcom_sim7080
#include <zephyr/logging/log.h>
#include <zephyr/net/offloaded_netdev.h>
LOG_MODULE_REGISTER(modem_simcom_sim7080_sock, CONFIG_MODEM_LOG_LEVEL);
#include <zephyr/drivers/modem/simcom-sim7080.h>
#include "sim7080.h"
static void socket_close(struct modem_socket *sock);
/*
* Parses the +CAOPEN command and gives back the
* connect semaphore.
*/
MODEM_CMD_DEFINE(on_cmd_caopen)
{
int result = atoi(argv[1]);
LOG_INF("+CAOPEN: %d", result);
modem_cmd_handler_set_error(data, result);
return 0;
}
/*
* Connects an modem socket. Protocol can either be TCP or UDP.
*/
static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t addrlen)
{
struct modem_socket *sock = (struct modem_socket *)obj;
uint16_t dst_port = 0;
char *protocol;
struct modem_cmd cmd[] = { MODEM_CMD("+CAOPEN: ", on_cmd_caopen, 2U, ",") };
char buf[sizeof("AT+CAOPEN: #,#,#####,"
"#xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxx.xxx.xxx.xxx#,####")];
char ip_str[NET_IPV6_ADDR_LEN];
int ret;
/* Modem is not attached to the network. */
if (sim7080_get_state() != SIM7080_STATE_NETWORKING) {
return -EAGAIN;
}
if (modem_socket_is_allocated(&mdata.socket_config, sock) == false) {
LOG_ERR("Invalid socket id %d from fd %d", sock->id, sock->sock_fd);
errno = EINVAL;
return -1;
}
if (sock->is_connected == true) {
LOG_ERR("Socket is already connected! id: %d, fd: %d", sock->id, sock->sock_fd);
errno = EISCONN;
return -1;
}
/* get the destination port */
if (addr->sa_family == AF_INET6) {
dst_port = ntohs(net_sin6(addr)->sin6_port);
} else if (addr->sa_family == AF_INET) {
dst_port = ntohs(net_sin(addr)->sin_port);
}
/* Get protocol */
protocol = (sock->type == SOCK_STREAM) ? "TCP" : "UDP";
ret = modem_context_sprint_ip_addr(addr, ip_str, sizeof(ip_str));
if (ret != 0) {
LOG_ERR("Failed to format IP!");
errno = ENOMEM;
return -1;
}
ret = snprintk(buf, sizeof(buf), "AT+CAOPEN=%d,%d,\"%s\",\"%s\",%d", 0, sock->id,
protocol, ip_str, dst_port);
if (ret < 0) {
LOG_ERR("Failed to build connect command. ID: %d, FD: %d", sock->id, sock->sock_fd);
errno = ENOMEM;
return -1;
}
ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmd, ARRAY_SIZE(cmd), buf,
&mdata.sem_response, MDM_CONNECT_TIMEOUT);
if (ret < 0) {
LOG_ERR("%s ret: %d", buf, ret);
socket_close(sock);
goto error;
}
ret = modem_cmd_handler_get_error(&mdata.cmd_handler_data);
if (ret != 0) {
LOG_ERR("Closing the socket!");
socket_close(sock);
goto error;
}
sock->is_connected = true;
errno = 0;
return 0;
error:
errno = -ret;
return -1;
}
/*
* Send data over a given socket.
*
* First we signal the module that we want to send data over a socket.
* This is done by sending AT+CASEND=<sockfd>,<nbytes>\r\n.
* If The module is ready to send data it will send back
* an UNTERMINATED prompt '> '. After that data can be sent to the modem.
* As terminating byte a STRG+Z (0x1A) is sent. The module will
* then send a OK or ERROR.
*/
static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)
{
int ret;
struct modem_socket *sock = (struct modem_socket *)obj;
char send_buf[sizeof("AT+CASEND=#,####")] = { 0 };
char ctrlz = 0x1A;
/* Modem is not attached to the network. */
if (sim7080_get_state() != SIM7080_STATE_NETWORKING) {
LOG_ERR("Modem currently not attached to the network!");
return -EAGAIN;
}
/* Do some sanity checks. */
if (!buf || len == 0) {
errno = EINVAL;
return -1;
}
/* Socket has to be connected. */
if (!sock->is_connected) {
errno = ENOTCONN;
return -1;
}
/* Only send up to MTU bytes. */
if (len > MDM_MAX_DATA_LENGTH) {
len = MDM_MAX_DATA_LENGTH;
}
ret = snprintk(send_buf, sizeof(send_buf), "AT+CASEND=%d,%ld", sock->id, (long)len);
if (ret < 0) {
LOG_ERR("Failed to build send command!!");
errno = ENOMEM;
return -1;
}
/* Make sure only one send can be done at a time. */
k_sem_take(&mdata.cmd_handler_data.sem_tx_lock, K_FOREVER);
k_sem_reset(&mdata.sem_tx_ready);
/* Send CASEND */
mdata.current_sock_written = len;
ret = modem_cmd_send_nolock(&mctx.iface, &mctx.cmd_handler, NULL, 0U, send_buf, NULL,
K_NO_WAIT);
if (ret < 0) {
LOG_ERR("Failed to send CASEND!!");
goto exit;
}
/* Wait for '> ' */
ret = k_sem_take(&mdata.sem_tx_ready, K_SECONDS(2));
if (ret < 0) {
LOG_ERR("Timeout while waiting for tx");
goto exit;
}
/* Send data */
modem_cmd_send_data_nolock(&mctx.iface, buf, len);
modem_cmd_send_data_nolock(&mctx.iface, &ctrlz, 1);
/* Wait for the OK */
k_sem_reset(&mdata.sem_response);
ret = k_sem_take(&mdata.sem_response, MDM_CMD_TIMEOUT);
if (ret < 0) {
LOG_ERR("Timeout waiting for OK");
}
exit:
k_sem_give(&mdata.cmd_handler_data.sem_tx_lock);
/* Data was successfully sent */
if (ret < 0) {
errno = -ret;
return -1;
}
errno = 0;
return mdata.current_sock_written;
}
/*
* Read data from a given socket.
*
* The response has the form +CARECV: <length>,data\r\nOK\r\n
*/
static int sockread_common(int sockfd, struct modem_cmd_handler_data *data, int socket_data_length,
uint16_t len)
{
struct modem_socket *sock;
struct socket_read_data *sock_data;
int ret, packet_size;
if (!len) {
LOG_ERR("Invalid length, aborting");
return -EAGAIN;
}
if (!data->rx_buf) {
LOG_ERR("Incorrect format! Ignoring data!");
return -EINVAL;
}
if (socket_data_length <= 0) {
LOG_ERR("Length error (%d)", socket_data_length);
return -EAGAIN;
}
if (net_buf_frags_len(data->rx_buf) < socket_data_length) {
LOG_DBG("Not enough data -- wait!");
return -EAGAIN;
}
sock = modem_socket_from_fd(&mdata.socket_config, sockfd);
if (!sock) {
LOG_ERR("Socket not found! (%d)", sockfd);
ret = -EINVAL;
goto exit;
}
sock_data = (struct socket_read_data *)sock->data;
if (!sock_data) {
LOG_ERR("Socket data not found! (%d)", sockfd);
ret = -EINVAL;
goto exit;
}
ret = net_buf_linearize(sock_data->recv_buf, sock_data->recv_buf_len, data->rx_buf, 0,
(uint16_t)socket_data_length);
data->rx_buf = net_buf_skip(data->rx_buf, ret);
sock_data->recv_read_len = ret;
if (ret != socket_data_length) {
LOG_ERR("Total copied data is different then received data!"
" copied:%d vs. received:%d",
ret, socket_data_length);
ret = -EINVAL;
goto exit;
}
exit:
/* Indication only sets length to a dummy value. */
packet_size = modem_socket_next_packet_size(&mdata.socket_config, sock);
modem_socket_packet_size_update(&mdata.socket_config, sock, -packet_size);
return ret;
}
/*
* Handler for carecv response.
*/
MODEM_CMD_DEFINE(on_cmd_carecv)
{
return sockread_common(mdata.current_sock_fd, data, atoi(argv[0]), len);
}
/*
* Read data from a given socket.
*/
static ssize_t offload_recvfrom(void *obj, void *buf, size_t max_len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen)
{
struct modem_socket *sock = (struct modem_socket *)obj;
char sendbuf[sizeof("AT+CARECV=##,####")];
int ret, packet_size;
struct socket_read_data sock_data;
struct modem_cmd data_cmd[] = { MODEM_CMD("+CARECV: ", on_cmd_carecv, 1U, ",") };
/* Modem is not attached to the network. */
if (sim7080_get_state() != SIM7080_STATE_NETWORKING) {
LOG_ERR("Modem currently not attached to the network!");
return -EAGAIN;
}
if (!buf || max_len == 0) {
errno = EINVAL;
return -1;
}
if (flags & ZSOCK_MSG_PEEK) {
errno = ENOTSUP;
return -1;
}
packet_size = modem_socket_next_packet_size(&mdata.socket_config, sock);
if (!packet_size) {
if (flags & ZSOCK_MSG_DONTWAIT) {
errno = EAGAIN;
return -1;
}
modem_socket_wait_data(&mdata.socket_config, sock);
packet_size = modem_socket_next_packet_size(&mdata.socket_config, sock);
}
max_len = (max_len > MDM_MAX_DATA_LENGTH) ? MDM_MAX_DATA_LENGTH : max_len;
snprintk(sendbuf, sizeof(sendbuf), "AT+CARECV=%d,%zd", sock->id, max_len);
memset(&sock_data, 0, sizeof(sock_data));
sock_data.recv_buf = buf;
sock_data.recv_buf_len = max_len;
sock_data.recv_addr = src_addr;
sock->data = &sock_data;
mdata.current_sock_fd = sock->sock_fd;
ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, data_cmd, ARRAY_SIZE(data_cmd),
sendbuf, &mdata.sem_response, MDM_CMD_TIMEOUT);
if (ret < 0) {
errno = -ret;
ret = -1;
goto exit;
}
/* HACK: use dst address as src */
if (src_addr && addrlen) {
*addrlen = sizeof(sock->dst);
memcpy(src_addr, &sock->dst, *addrlen);
}
errno = 0;
ret = sock_data.recv_read_len;
exit:
/* clear socket data */
mdata.current_sock_fd = -1;
sock->data = NULL;
return ret;
}
/*
* Sends messages to the modem.
*/
static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags)
{
struct modem_socket *sock = obj;
ssize_t sent = 0;
const char *buf;
size_t len;
int ret;
/* Modem is not attached to the network. */
if (sim7080_get_state() != SIM7080_STATE_NETWORKING) {
LOG_ERR("Modem currently not attached to the network!");
return -EAGAIN;
}
if (sock->type == SOCK_DGRAM) {
/*
* Current implementation only handles single contiguous fragment at a time, so
* prevent sending multiple datagrams.
*/
if (msghdr_non_empty_iov_count(msg) > 1) {
errno = EMSGSIZE;
return -1;
}
}
for (int i = 0; i < msg->msg_iovlen; i++) {
buf = msg->msg_iov[i].iov_base;
len = msg->msg_iov[i].iov_len;
while (len > 0) {
ret = offload_sendto(obj, buf, len, flags, msg->msg_name, msg->msg_namelen);
if (ret < 0) {
if (ret == -EAGAIN) {
k_sleep(K_SECONDS(1));
} else {
return ret;
}
} else {
sent += ret;
buf += ret;
len -= ret;
}
}
}
return sent;
}
/*
* Closes a given socket.
*/
static void socket_close(struct modem_socket *sock)
{
char buf[sizeof("AT+CACLOSE=##")];
int ret;
snprintk(buf, sizeof(buf), "AT+CACLOSE=%d", sock->id);
ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buf, &mdata.sem_response,
MDM_CMD_TIMEOUT);
if (ret < 0) {
LOG_ERR("%s ret: %d", buf, ret);
}
modem_socket_put(&mdata.socket_config, sock->sock_fd);
}
/*
* Offloads read by reading from a given socket.
*/
static ssize_t offload_read(void *obj, void *buffer, size_t count)
{
return offload_recvfrom(obj, buffer, count, 0, NULL, 0);
}
/*
* Offloads write by writing to a given socket.
*/
static ssize_t offload_write(void *obj, const void *buffer, size_t count)
{
return offload_sendto(obj, buffer, count, 0, NULL, 0);
}
/*
* Offloads close by terminating the connection and freeing the socket.
*/
static int offload_close(void *obj)
{
struct modem_socket *sock = (struct modem_socket *)obj;
/* Modem is not attached to the network. */
if (sim7080_get_state() != SIM7080_STATE_NETWORKING) {
LOG_ERR("Modem currently not attached to the network!");
return -EAGAIN;
}
/* Make sure socket is allocated */
if (modem_socket_is_allocated(&mdata.socket_config, sock) == false) {
return 0;
}
/* Close the socket only if it is connected. */
if (sock->is_connected) {
socket_close(sock);
}
return 0;
}
/*
* Polls a given socket.
*/
static int offload_poll(struct zsock_pollfd *fds, int nfds, int msecs)
{
int i;
void *obj;
/* Modem is not attached to the network. */
if (sim7080_get_state() != SIM7080_STATE_NETWORKING) {
LOG_ERR("Modem currently not attached to the network!");
return -EAGAIN;
}
/* Only accept modem sockets. */
for (i = 0; i < nfds; i++) {
if (fds[i].fd < 0) {
continue;
}
/* If vtable matches, then it's modem socket. */
obj = zvfs_get_fd_obj(fds[i].fd,
(const struct fd_op_vtable *)&offload_socket_fd_op_vtable,
EINVAL);
if (obj == NULL) {
return -1;
}
}
return modem_socket_poll(&mdata.socket_config, fds, nfds, msecs);
}
/*
* Offloads ioctl. Only supported ioctl is poll_offload.
*/
static int offload_ioctl(void *obj, unsigned int request, va_list args)
{
switch (request) {
case ZFD_IOCTL_POLL_PREPARE:
return -EXDEV;
case ZFD_IOCTL_POLL_UPDATE:
return -EOPNOTSUPP;
case ZFD_IOCTL_POLL_OFFLOAD: {
/* Poll on the given socket. */
struct zsock_pollfd *fds;
int nfds, timeout;
fds = va_arg(args, struct zsock_pollfd *);
nfds = va_arg(args, int);
timeout = va_arg(args, int);
return offload_poll(fds, nfds, timeout);
}
default:
errno = EINVAL;
return -1;
}
}
const struct socket_op_vtable offload_socket_fd_op_vtable = {
.fd_vtable = {
.read = offload_read,
.write = offload_write,
.close = offload_close,
.ioctl = offload_ioctl,
},
.bind = NULL,
.connect = offload_connect,
.sendto = offload_sendto,
.recvfrom = offload_recvfrom,
.listen = NULL,
.accept = NULL,
.sendmsg = offload_sendmsg,
.getsockopt = NULL,
.setsockopt = NULL,
};