blob: 9840ea906a36bb1a5098f542fa433f0d7db56a50 [file] [log] [blame]
/*
* Copyright (c) 2025 Netfeasa Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/modem/chat.h>
#include <zephyr/modem/backend/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_offload.h>
#include <zephyr/net/offloaded_netdev.h>
#include <zephyr/net/socket_offload.h>
#include <zephyr/posix/fcntl.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) && defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS)
#include "tls_internal.h"
#include <zephyr/net/tls_credentials.h>
#endif
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include "hl78xx.h"
#include "hl78xx_chat.h"
#include "hl78xx_cfg.h"
LOG_MODULE_REGISTER(hl78xx_socket, CONFIG_MODEM_LOG_LEVEL);
/*
* hl78xx_sockets.c
*
* Responsibilities:
* - Provide the socket offload integration for the HL78xx modem.
* - Parse modem URC/chat replies used to transfer payloads over the UART pipe.
* - Format and send AT commands for socket lifecycle (create, connect, send, recv,
* close, delete) and handle their confirmation/URC callbacks.
* - Provide TLS credential handling when enabled.
*/
/* Helper macros and constants */
#define MODEM_STREAM_STARTER_WORD "\r\n" CONNECT_STRING "\r\n"
#define MODEM_STREAM_END_WORD "\r\n" OK_STRING "\r\n"
#define MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT (0)
#define HL78XX_UART_PIPE_WORK_SOCKET_BUFFER_SIZE 32
/* modem socket id is 1-based */
#define HL78XX_TCP_STATUS_ID(x) ((x > 1 ? (x) - 1 : 0))
/* modem socket id is 1-based */
#define HL78XX_UDP_STATUS_ID(x) ((x > 1 ? (x) - 1 : 0))
#define DNS_SERVERS_COUNT \
(0 + (IS_ENABLED(CONFIG_NET_IPV6) ? 1 : 0) + (IS_ENABLED(CONFIG_NET_IPV4) ? 1 : 0) + \
1 /* for NULL terminator */ \
)
RING_BUF_DECLARE(mdm_recv_pool, CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES);
struct hl78xx_dns_info {
#ifdef CONFIG_NET_IPV4
char v4_string[NET_IPV4_ADDR_LEN];
struct in_addr v4;
#endif
#ifdef CONFIG_NET_IPV6
char v6_string[NET_IPV6_ADDR_LEN];
struct in6_addr v6;
#endif
bool ready;
};
/* IPv4 information is optional and only present when IPv4 is enabled */
#ifdef CONFIG_NET_IPV4
struct hl78xx_ipv4_info {
struct in_addr addr;
struct in_addr subnet;
struct in_addr gateway;
struct in_addr new_addr;
};
#endif
/* IPv6 information is optional and only present when IPv6 is enabled */
#ifdef CONFIG_NET_IPV6
struct hl78xx_ipv6_info {
struct in6_addr addr;
struct in6_addr subnet;
struct in6_addr gateway;
struct in6_addr new_addr;
};
#endif
/* TLS information is optional and only present when TLS is enabled */
struct hl78xx_tls_info {
char hostname[MDM_MAX_HOSTNAME_LEN];
bool hostname_set;
};
enum hl78xx_tcp_socket_status_code {
/** Error occurred, socket is not usable */
TCP_SOCKET_ERROR = 0,
/** Connection is up, socket can be used to send/receive data */
TCP_SOCKET_CONNECTED,
};
enum hl78xx_udp_socket_status_code {
UDP_SOCKET_ERROR = 0, /* Error occurred, socket is not usable */
/** Connection is up, socket can be used to send/receive data */
UDP_SOCKET_CREATED,
};
struct hl78xx_tcp_status {
enum hl78xx_tcp_socket_status_code err_code;
bool is_connected;
bool is_created;
};
struct hl78xx_udp_status {
enum hl78xx_udp_socket_status_code err_code;
bool is_created;
};
struct receive_socket_data {
char buf[MDM_MAX_DATA_LENGTH + ARRAY_SIZE(MODEM_STREAM_STARTER_WORD) +
ARRAY_SIZE(MODEM_STREAM_END_WORD)];
uint16_t len;
};
struct hl78xx_socket_data {
struct net_if *net_iface;
uint8_t mac_addr[6];
/* socket data */
struct modem_socket_config socket_config;
struct modem_socket sockets[MDM_MAX_SOCKETS];
int current_sock_fd;
int sizeof_socket_data;
int requested_socket_id;
bool socket_data_error;
#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6)
struct hl78xx_dns_info dns;
#endif
#ifdef CONFIG_NET_IPV4
struct hl78xx_ipv4_info ipv4;
#endif
#ifdef CONFIG_NET_IPV6
struct hl78xx_ipv6_info ipv6;
#endif
/* rx net buffer */
struct ring_buf *buf_pool;
uint32_t expected_buf_len;
uint32_t collected_buf_len;
struct receive_socket_data receive_buf;
/* device information */
const struct device *modem_dev;
const struct device *offload_dev;
struct hl78xx_data *mdata_global;
/* socket state */
struct hl78xx_tls_info tls;
struct hl78xx_tcp_status tcp_conn_status[MDM_MAX_SOCKETS];
struct hl78xx_udp_status udp_conn_status[MDM_MAX_SOCKETS];
/* per-socket parser state (migrated from globals) - use a small enum to
* make the parser's intent explicit and easier to read.
*/
enum {
HL78XX_PARSER_IDLE = 0,
HL78XX_PARSER_CONNECT_MATCHED,
HL78XX_PARSER_EOF_OK_MATCHED,
HL78XX_PARSER_ERROR_MATCHED,
} parser_state;
/* transient: prevents further parsing until parser_reset clears it */
bool parser_match_found;
uint16_t parser_start_index_eof;
uint16_t parser_size_of_socketdata;
/* true once payload has been pushed into ring_buf */
bool parser_socket_data_received;
/* set when EOF pattern was found and payload pushed */
bool parser_eof_detected;
/* set when OK token was matched after payload */
bool parser_ok_detected;
};
static struct hl78xx_socket_data *socket_data_global;
/* ===== Utils ==========================================================
* Small, stateless utility helpers used across this file.
* Grouping here reduces cognitive load when navigating the file.
*/
static inline void hl78xx_set_socket_global(struct hl78xx_socket_data *d)
{
socket_data_global = d;
}
static inline struct hl78xx_socket_data *hl78xx_get_socket_global(void)
{
return socket_data_global;
}
/* Helper: map an internal return code into POSIX errno and set errno.
* - negative values are assumed to be negative errno semantics -> map to positive
* - positive values are assumed already POSIX errno -> pass through
* - zero or unknown -> fallback to EIO
*/
static inline void hl78xx_set_errno_from_code(int code)
{
if (code < 0) {
errno = -code;
} else if (code > 0) {
errno = code;
} else {
errno = EIO;
}
}
/* ===== Forward declarations ==========================================
* Group commonly used static helper prototypes here so callers can be
* reordered without implicit-declaration warnings. Keep this section
* compact. When moving functions into groups, add any new prototypes
* here first.
*/
static void check_tcp_state_if_needed(struct hl78xx_socket_data *socket_data,
struct modem_socket *sock);
/* Parser helpers */
static bool split_ipv4_and_subnet(const char *combined, char *ip_out, size_t ip_out_len,
char *subnet_out, size_t subnet_out_len);
static bool parse_ip(bool is_ipv4, const char *ip_str, void *out_addr);
static bool update_dns(struct hl78xx_socket_data *socket_data, bool is_ipv4, const char *dns_str);
static void set_iface(struct hl78xx_socket_data *socket_data, bool is_ipv4);
static void parser_reset(struct hl78xx_socket_data *socket_data);
static void found_reset(struct hl78xx_socket_data *socket_data);
static bool modem_chat_parse_end_del_start(struct hl78xx_socket_data *socket_data,
struct modem_chat *chat);
static bool modem_chat_parse_end_del_complete(struct hl78xx_socket_data *socket_data,
struct modem_chat *chat);
static bool modem_chat_match_matches_received(struct hl78xx_socket_data *socket_data,
const char *match, uint16_t match_size);
/* Receive / parser entrypoints */
static void socket_process_bytes(struct hl78xx_socket_data *socket_data, char byte);
static int modem_process_handler(struct hl78xx_data *data);
static void modem_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event,
void *user_data);
/* Socket I/O helpers */
static int on_cmd_sockread_common(int socket_id, uint16_t socket_data_length, uint16_t len,
void *user_data);
static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, struct sockaddr *from,
socklen_t *fromlen);
static int prepare_send_cmd(const struct modem_socket *sock, const struct sockaddr *dst_addr,
size_t buf_len, char *cmd_buf, size_t cmd_buf_size);
static int send_data_buffer(struct hl78xx_socket_data *socket_data, const char *buf,
const size_t buf_len, int *sock_written);
/* Socket lifecycle */
static int create_socket(struct modem_socket *sock, const struct sockaddr *addr,
struct hl78xx_socket_data *data);
static int socket_close(struct hl78xx_socket_data *socket_data, struct modem_socket *sock);
static int socket_delete(struct hl78xx_socket_data *socket_data, struct modem_socket *sock);
static void socket_notify_data(int socket_id, int new_total, void *user_data);
/* ===== TLS prototypes (conditional) ==================================
* Forward declarations for TLS-related helpers. Grouped separately so
* TLS-specific code paths are easy to find.
*/
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) && defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS)
static int map_credentials(struct hl78xx_socket_data *socket_data, const void *optval,
socklen_t optlen);
static int hl78xx_configure_chipper_suit(struct hl78xx_socket_data *socket_data);
#endif /* CONFIG_NET_SOCKETS_SOCKOPT_TLS */
/* ===== Container helpers =============================================
* Small helpers used to map between container structures and their
* member pointers (eg. `modem_socket` -> `hl78xx_socket_data`).
*/
static inline struct hl78xx_socket_data *hl78xx_socket_data_from_sock(struct modem_socket *sock)
{
/* Robustly recover the parent `hl78xx_socket_data` for any element
* address within the `sockets[]` array. Using CONTAINER_OF with
* `sockets[0]` is not safe when `sock` points to `sockets[i]` (i>0),
* because CONTAINER_OF assumes the pointer is to the member named
* in the macro (sockets[0]). That yields a pointer offset by
* i * sizeof(sockets[0]).
*
* Strategy: for each possible index i, compute the candidate parent
* base address so that &candidate->sockets[i] == sock. If the math
* yields a candidate that looks like a valid container, return it.
*/
if (!sock) {
return NULL;
}
const size_t elem_size = sizeof(((struct hl78xx_socket_data *)0)->sockets[0]);
const size_t sockets_off = offsetof(struct hl78xx_socket_data, sockets);
struct hl78xx_socket_data *result = NULL;
for (int i = 0; i < MDM_MAX_SOCKETS; i++) {
struct hl78xx_socket_data *candidate =
(struct hl78xx_socket_data *)((char *)sock -
(ptrdiff_t)(sockets_off +
(size_t)i * elem_size));
/* Quick sanity: does candidate->sockets[i] point back to sock? */
if ((struct modem_socket *)&candidate->sockets[i] != sock) {
continue;
}
if (candidate->offload_dev && candidate->mdata_global) {
return candidate;
}
/* Remember the first match as a fallback */
if (!result) {
result = candidate;
}
}
return result;
}
/* ===== Chat callbacks (grouped) =====================================
* Group all chat/URC handlers together to make the socket TU easier to
* scan. These handlers are registered via hl78xx_chat getters in
* `hl78xx_chat.c` and forward URC context into the socket layer.
*/
void hl78xx_on_socknotifydata(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data)
{
int socket_id = -1;
int new_total = -1;
if (argc < 2) {
return;
}
socket_id = ATOI(argv[1], -1, "socket_id");
new_total = ATOI(argv[2], -1, "length");
if (socket_id < 0 || new_total < 0) {
return;
}
HL78XX_LOG_DBG("%d %d %d", __LINE__, socket_id, new_total);
/* Notify the socket layer that data is available */
socket_notify_data(socket_id, new_total, user_data);
}
/** +KTCP_NOTIF: <session_id>, <tcp_notif> */
void hl78xx_on_ktcpnotif(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data)
{
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
enum hl78xx_tcp_notif tcp_notif_received;
int socket_id = -1;
int tcp_notif = -1;
if (!data || !socket_data) {
LOG_ERR("%s: invalid user_data", __func__);
return;
}
if (argc < 2) {
return;
}
socket_id = ATOI(argv[1], -1, "socket_id");
tcp_notif = ATOI(argv[2], -1, "tcp_notif");
if (tcp_notif == -1) {
return;
}
tcp_notif_received = (enum hl78xx_tcp_notif)tcp_notif;
/* Store the socket id for the notification */
socket_data->requested_socket_id = socket_id;
switch (tcp_notif_received) {
case TCP_NOTIF_REMOTE_DISCONNECTION:
/**
* To Handle remote disconnection
* give a dummy packet size of 1
*
*/
socket_notify_data(socket_id, 1, user_data);
break;
case TCP_NOTIF_NETWORK_ERROR:
/* Handle network error */
break;
default:
break;
}
}
void hl78xx_on_ktcpind(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data)
{
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
struct modem_socket *sock = NULL;
int socket_id = -1;
int tcp_conn_stat = -1;
if (!data || !socket_data) {
LOG_ERR("%s: invalid user_data", __func__);
return;
}
if (argc < 3 || !argv[1] || !argv[2]) {
LOG_ERR("TCP_IND: Incomplete response");
goto exit;
}
socket_id = ATOI(argv[1], -1, "socket_id");
if (socket_id == -1) {
goto exit;
}
sock = modem_socket_from_id(&socket_data->socket_config, socket_id);
tcp_conn_stat = ATOI(argv[2], -1, "tcp_status");
if (tcp_conn_stat == TCP_SOCKET_CONNECTED) {
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].err_code =
tcp_conn_stat;
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_connected = true;
return;
}
exit:
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].err_code = tcp_conn_stat;
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_connected = false;
if (socket_id != -1) {
modem_socket_put(&socket_data->socket_config, sock->sock_fd);
}
}
/* Chat/URC handler for socket-create/indication responses
* Matches +KTCPCFG: <id>
*/
void hl78xx_on_ktcpsocket_create(struct modem_chat *chat, char **argv, uint16_t argc,
void *user_data)
{
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
struct modem_socket *sock = NULL;
int socket_id = -1;
if (!data || !socket_data) {
LOG_ERR("%s: invalid user_data", __func__);
return;
}
if (argc < 2 || !argv[1]) {
LOG_ERR("%s: Incomplete response", __func__);
goto exit;
}
/* argv[0] may contain extra CSV fields; parse leading integer */
socket_id = ATOI(argv[1], -1, "socket_id");
if (socket_id <= 0) {
LOG_DBG("unable to parse socket id from '%s'", argv[1]);
goto exit;
}
/* Try to find a reserved/new socket slot and assign the modem-provided id. */
sock = modem_socket_from_newid(&socket_data->socket_config);
if (!sock) {
goto exit;
}
if (modem_socket_id_assign(&socket_data->socket_config, sock, socket_id) < 0) {
LOG_ERR("Failed to assign modem socket id %d to fd %d", socket_id, sock->sock_fd);
goto exit;
} else {
LOG_DBG("Assigned modem socket id %d to fd %d", socket_id, sock->sock_fd);
}
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_created = true;
return;
exit:
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].err_code = TCP_SOCKET_ERROR;
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_created = false;
if (socket_id != -1 && sock) {
modem_socket_put(&socket_data->socket_config, sock->sock_fd);
}
}
/* Chat/URC handler for socket-create/indication responses
* Matches +KUDPCFG: <id>
* +KUDP_IND: <id>,... (or +KTCP_IND)
*/
void hl78xx_on_kudpsocket_create(struct modem_chat *chat, char **argv, uint16_t argc,
void *user_data)
{
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
struct modem_socket *sock = NULL;
int socket_id = -1;
int udp_create_stat = -1;
if (!data || !socket_data) {
LOG_ERR("%s: invalid user_data", __func__);
return;
}
if (argc < 2 || !argv[1]) {
LOG_ERR("%s: Incomplete response", __func__);
goto exit;
}
/* argv[0] may contain extra CSV fields; parse leading integer */
socket_id = ATOI(argv[1], -1, "socket_id");
if (socket_id <= 0) {
LOG_DBG("unable to parse socket id from '%s'", argv[1]);
goto exit;
}
/* Try to find a reserved/new socket slot and assign the modem-provided id. */
sock = modem_socket_from_newid(&socket_data->socket_config);
if (!sock) {
goto exit;
}
if (modem_socket_id_assign(&socket_data->socket_config, sock, socket_id) < 0) {
LOG_ERR("Failed to assign modem socket id %d to fd %d", socket_id, sock->sock_fd);
goto exit;
} else {
LOG_DBG("Assigned modem socket id %d to fd %d", socket_id, sock->sock_fd);
}
/* Parse connection status: 1=created, otherwise=error */
udp_create_stat = ATOI(argv[2], 0, "udp_status");
if (udp_create_stat == UDP_SOCKET_CREATED) {
socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].err_code =
udp_create_stat;
socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].is_created = true;
return;
}
exit:
socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].err_code = UDP_SOCKET_ERROR;
socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].is_created = false;
if (socket_id != -1 && sock) {
modem_socket_put(&socket_data->socket_config, sock->sock_fd);
}
}
#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG
#ifdef CONFIG_MODEM_HL78XX_12
/**
* @brief Handle modem state update from +KSTATE URC of RAT Scan Finish.
* This command is intended to report events for different important state transitions and system
* occurrences.
* Actually this eventc'state is really important functionality to understand networks
* searching phase of the modem.
* Verbose debug logging for KSTATEV events
*/
void hl78xx_on_kstatev_parser(struct hl78xx_data *data, int state, int rat_mode)
{
switch (state) {
case EVENT_START_SCAN:
break;
case EVENT_FAIL_SCAN:
LOG_DBG("Modem failed to find a suitable network");
break;
case EVENT_ENTER_CAMPED:
LOG_DBG("Modem entered camped state on a suitable or acceptable cell");
break;
case EVENT_CONNECTION_ESTABLISHMENT:
LOG_DBG("Modem successfully established a connection to the network");
break;
case EVENT_START_RESCAN:
LOG_DBG("Modem is starting a rescan for available networks");
break;
case EVENT_RRC_CONNECTED:
LOG_DBG("Modem has established an RRC connection with the network");
break;
case EVENT_NO_SUITABLE_CELLS:
LOG_DBG("Modem did not find any suitable cells during the scan");
break;
case EVENT_ALL_REGISTRATION_FAILED:
LOG_DBG("Modem failed to register to any network");
break;
default:
LOG_DBG("Unhandled KSTATEV for state %d", state);
break;
}
}
#endif
/**
* @brief This function doesn't handle incoming UDP data.
* It is just a placeholder for verbose debug logging of incoming UDP data.
* +KUDP_RCV: <remote_addr>,<remote_port>,
*/
void hl78xx_on_udprcv(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data)
{
if (argc < 2) {
return;
}
HL78XX_LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]);
}
#endif
/* Handler for +CGCONTRDP: <cid>,<bearer>,<apn>,<addr>,<dcomp>,<hcomp>,<dns1>[,<dns2>]
* This function is invoked by the chat layer when a CGCONTRDP URC is matched.
* It extracts the PDP context address, gateway and DNS servers and updates the
* per-instance socket_data DNS fields so dns_work_cb() can apply them.
*/
void hl78xx_on_cgdcontrdp(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data)
{
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
const char *addr_field = NULL;
const char *gw_field = NULL;
const char *dns_field = NULL;
const char *apn_field = NULL;
/* Accept both comma-split argv[] or a single raw token that needs tokenizing */
if (argc >= 7) {
apn_field = argv[3];
addr_field = argv[4];
gw_field = argv[5];
dns_field = argv[6];
} else {
LOG_ERR("Incomplete CGCONTRDP response: argc=%d", argc);
return;
}
LOG_INF("Apn=%s", apn_field);
LOG_INF("Addr=%s", addr_field);
LOG_INF("Gw=%s", gw_field);
LOG_INF("DNS=%s", dns_field);
#ifdef CONFIG_MODEM_HL78XX_APN_SOURCE_NETWORK
if (apn_field) {
hl78xx_extract_essential_part_apn(apn_field, data->identity.apn,
sizeof(data->identity.apn));
}
#endif
/* Handle address parsing: IPv4 replies sometimes embed subnet as extra
* octets concatenated after the IP (e.g. "10.149.122.90.255.255.255.252").
* Split and parse into the instance IPv4 fields so the interface can be
* configured before the DNS resolver is invoked.
*/
#ifdef CONFIG_NET_IPV4
if (addr_field && strchr(addr_field, '.') && !strchr(addr_field, ':')) {
char ip_addr[NET_IPV6_ADDR_LEN] = {0};
char subnet_mask[NET_IPV6_ADDR_LEN] = {0};
if (!split_ipv4_and_subnet(addr_field, ip_addr, sizeof(ip_addr), subnet_mask,
sizeof(subnet_mask))) {
LOG_ERR("CGCONTRDP: failed to split IPv4+subnet: %s", addr_field);
return;
}
if (!parse_ip(true, ip_addr, &socket_data->ipv4.new_addr)) {
return;
}
if (!parse_ip(true, subnet_mask, &socket_data->ipv4.subnet)) {
return;
}
if (gw_field && !parse_ip(true, gw_field, &socket_data->ipv4.gateway)) {
return;
}
}
#else
ARG_UNUSED(gw_field);
#endif
#ifdef CONFIG_NET_IPV6
if (addr_field && strchr(addr_field, ':') &&
!parse_ip(false, addr_field, &socket_data->ipv6.new_addr)) {
return;
}
#endif
/* Update DNS and configure interface */
if (!update_dns(socket_data,
#ifdef CONFIG_NET_IPV4
(addr_field && strchr(addr_field, '.') && !strchr(addr_field, ':')),
#else
false,
#endif
dns_field ? dns_field : "")) {
return;
}
/* Configure the interface addresses so net_if_is_up()/address selection
* will succeed before attempting to reconfigure the resolver.
*/
#ifdef CONFIG_NET_IPV4
set_iface(socket_data, (addr_field && strchr(addr_field, '.') && !strchr(addr_field, ':')));
#elif defined(CONFIG_NET_IPV6)
set_iface(socket_data, false);
#endif
socket_data->dns.ready = false;
LOG_DBG("CGCONTRDP processed, dns strings: v4=%s v6=%s",
#ifdef CONFIG_NET_IPV4
socket_data->dns.v4_string,
#else
"<no-v4>",
#endif
#ifdef CONFIG_NET_IPV6
socket_data->dns.v6_string
#else
"<no-v6>"
#endif
);
}
/* ===== Network / Parsing Utilities ===================================
* Helpers that operate on IP address parsing and DNS/address helpers.
*/
static bool parse_ip(bool is_ipv4, const char *ip_str, void *out_addr)
{
int ret = net_addr_pton(is_ipv4 ? AF_INET : AF_INET6, ip_str, out_addr);
LOG_DBG("Parsing %s address: %s -> %s", is_ipv4 ? "IPv4" : "IPv6", ip_str,
(ret < 0) ? "FAIL" : "OK");
if (ret < 0) {
LOG_ERR("Invalid IP address: %s", ip_str);
return false;
}
return true;
}
static bool update_dns(struct hl78xx_socket_data *socket_data, bool is_ipv4, const char *dns_str)
{
int ret;
/* ===== Interface helpers ==============================================
* Helpers that configure the network interface for IPv4/IPv6.
*/
LOG_DBG("Updating DNS (%s): %s", is_ipv4 ? "IPv4" : "IPv6", dns_str);
#ifdef CONFIG_NET_IPV4
if (is_ipv4) {
ret = strncmp(dns_str, socket_data->dns.v4_string, strlen(dns_str));
if (ret != 0) {
LOG_DBG("New IPv4 DNS differs from current, marking dns_ready = false");
socket_data->dns.ready = false;
}
strncpy(socket_data->dns.v4_string, dns_str, sizeof(socket_data->dns.v4_string));
socket_data->dns.v4_string[sizeof(socket_data->dns.v4_string) - 1] = '\0';
return parse_ip(true, socket_data->dns.v4_string, &socket_data->dns.v4);
}
#else
if (is_ipv4) {
LOG_DBG("IPv4 DNS reported but IPv4 disabled in build; ignoring");
return false;
}
#endif /* CONFIG_NET_IPV4 */
#ifdef CONFIG_NET_IPV6
else {
ret = strncmp(dns_str, socket_data->dns.v6_string, strlen(dns_str));
if (ret != 0) {
LOG_DBG("New IPv6 DNS differs from current, marking dns_ready = false");
socket_data->dns.ready = false;
}
strncpy(socket_data->dns.v6_string, dns_str, sizeof(socket_data->dns.v6_string));
socket_data->dns.v6_string[sizeof(socket_data->dns.v6_string) - 1] = '\0';
if (!parse_ip(false, socket_data->dns.v6_string, &socket_data->dns.v6)) {
return false;
}
net_addr_ntop(AF_INET6, &socket_data->dns.v6, socket_data->dns.v6_string,
sizeof(socket_data->dns.v6_string));
LOG_DBG("Parsed IPv6 DNS: %s", socket_data->dns.v6_string);
}
#endif /* CONFIG_NET_IPV6 */
return true;
}
static void set_iface(struct hl78xx_socket_data *socket_data, bool is_ipv4)
{
if (!socket_data->net_iface) {
LOG_DBG("No network interface set. Skipping iface config.");
return;
}
LOG_DBG("Setting %s interface address...", is_ipv4 ? "IPv4" : "IPv6");
if (is_ipv4) {
#ifdef CONFIG_NET_IPV4
if (socket_data->ipv4.addr.s_addr != 0) {
net_if_ipv4_addr_rm(socket_data->net_iface, &socket_data->ipv4.addr);
}
/* Use MANUAL so the stack treats this as a configured address and it is
* available for source address selection immediately.
*/
if (!net_if_ipv4_addr_add(socket_data->net_iface, &socket_data->ipv4.new_addr,
NET_ADDR_MANUAL, 0)) {
LOG_ERR("Failed to set IPv4 interface address.");
}
net_if_ipv4_set_netmask_by_addr(socket_data->net_iface, &socket_data->ipv4.new_addr,
&socket_data->ipv4.subnet);
net_if_ipv4_set_gw(socket_data->net_iface, &socket_data->ipv4.gateway);
net_ipaddr_copy(&socket_data->ipv4.addr, &socket_data->ipv4.new_addr);
LOG_DBG("IPv4 interface configuration complete.");
(void)net_if_up(socket_data->net_iface);
#else
LOG_DBG("IPv4 disabled: skipping IPv4 interface configuration");
#endif /* CONFIG_NET_IPV4 */
}
#ifdef CONFIG_NET_IPV6
else {
net_if_ipv6_addr_rm(socket_data->net_iface, &socket_data->ipv6.addr);
if (!net_if_ipv6_addr_add(socket_data->net_iface, &socket_data->ipv6.new_addr,
NET_ADDR_MANUAL, 0)) {
LOG_ERR("Failed to set IPv6 interface address.");
} else {
LOG_DBG("IPv6 interface configuration complete.");
}
/* Ensure iface up after adding address */
(void)net_if_up(socket_data->net_iface);
}
#endif /* CONFIG_NET_IPV6 */
}
static bool split_ipv4_and_subnet(const char *combined, char *ip_out, size_t ip_out_len,
char *subnet_out, size_t subnet_out_len)
{
int dot_count = 0;
const char *ptr = combined;
const char *split = NULL;
size_t ip_len = 0;
while (*ptr && dot_count < 4) {
if (*ptr == '.') {
dot_count++;
if (dot_count == 4) {
split = ptr;
break;
}
}
ptr++;
}
if (!split) {
LOG_ERR("Invalid IPv4 + subnet format: %s", combined);
return false;
}
ip_len = split - combined;
if (ip_len >= ip_out_len) {
ip_len = ip_out_len - 1;
}
strncpy(ip_out, combined, ip_len);
ip_out[ip_len] = '\0';
strncpy(subnet_out, split + 1, subnet_out_len);
subnet_out[subnet_out_len - 1] = '\0';
LOG_DBG("Extracted IP: %s, Subnet: %s", ip_out, subnet_out);
return true;
}
/* ===== Validation ====================================================
* Small validation helpers used by send/recv paths.
*/
static int validate_socket(const struct modem_socket *sock, struct hl78xx_socket_data *socket_data)
{
if (!sock) {
errno = EINVAL;
return -1;
}
bool not_connected = (!sock->is_connected && sock->type != SOCK_DGRAM);
bool tcp_disconnected =
(sock->type == SOCK_STREAM &&
!socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].is_connected);
bool udp_not_created =
(sock->type == SOCK_DGRAM &&
!socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(sock->id)].is_created);
if (not_connected || tcp_disconnected || udp_not_created) {
errno = ENOTCONN;
return -1;
}
return 0;
}
/* ===== Parser helpers ================================================
* Helpers that implement the streaming parser for incoming socket payloads
* and chat end-delimiter/EOF matching logic.
*/
static void parser_reset(struct hl78xx_socket_data *socket_data)
{
memset(&socket_data->receive_buf, 0, sizeof(socket_data->receive_buf));
socket_data->parser_match_found = false;
}
static void found_reset(struct hl78xx_socket_data *socket_data)
{
if (!socket_data) {
return;
}
/* Clear all parser progress state so a new transfer can start cleanly. */
socket_data->parser_state = HL78XX_PARSER_IDLE;
socket_data->parser_match_found = false;
socket_data->parser_socket_data_received = false;
socket_data->parser_eof_detected = false;
socket_data->parser_ok_detected = false;
}
static bool modem_chat_parse_end_del_start(struct hl78xx_socket_data *socket_data,
struct modem_chat *chat)
{
if (socket_data->receive_buf.len == 0) {
return false;
}
/* If the last received byte matches any of the delimiter bytes, we are
* starting the end-delimiter sequence. Use memchr to avoid an explicit
* loop and to be clearer about intent.
*/
return memchr(chat->delimiter,
socket_data->receive_buf.buf[socket_data->receive_buf.len - 1],
chat->delimiter_size) != NULL;
}
static bool modem_chat_parse_end_del_complete(struct hl78xx_socket_data *socket_data,
struct modem_chat *chat)
{
if (socket_data->receive_buf.len < chat->delimiter_size) {
return false;
}
return memcmp(&socket_data->receive_buf
.buf[socket_data->receive_buf.len - chat->delimiter_size],
chat->delimiter, chat->delimiter_size) == 0;
}
static bool modem_chat_match_matches_received(struct hl78xx_socket_data *socket_data,
const char *match, uint16_t match_size)
{
if (socket_data->receive_buf.len < match_size) {
return false;
}
return memcmp(socket_data->receive_buf.buf, match, match_size) == 0;
}
static bool is_receive_buffer_full(struct hl78xx_socket_data *socket_data)
{
return socket_data->receive_buf.len >= ARRAY_SIZE(socket_data->receive_buf.buf);
}
static void handle_expected_length_decrement(struct hl78xx_socket_data *socket_data)
{
/* Decrement expected length if CONNECT matched and expected length > 0 */
if (socket_data->parser_state == HL78XX_PARSER_CONNECT_MATCHED &&
socket_data->expected_buf_len > 0) {
socket_data->expected_buf_len--;
}
}
static bool is_end_delimiter_only(struct hl78xx_socket_data *socket_data)
{
return socket_data->receive_buf.len == socket_data->mdata_global->chat.delimiter_size;
}
static bool is_valid_eof_index(struct hl78xx_socket_data *socket_data, uint8_t size_match)
{
socket_data->parser_start_index_eof = socket_data->receive_buf.len - size_match - 2;
return socket_data->parser_start_index_eof < ARRAY_SIZE(socket_data->receive_buf.buf);
}
/* Handle EOF pattern: if EOF_PATTERN is found at the expected location,
* push socket payload (excluding EOF marker) into the ring buffer.
* Returns number of bytes pushed on success, 0 otherwise.
*/
static int handle_eof_pattern(struct hl78xx_socket_data *socket_data)
{
uint8_t size_match = strlen(EOF_PATTERN);
if (socket_data->receive_buf.len < size_match + 2) {
return 0;
}
if (!is_valid_eof_index(socket_data, size_match)) {
return 0;
}
if (strncmp(&socket_data->receive_buf.buf[socket_data->parser_start_index_eof], EOF_PATTERN,
size_match) == 0) {
int ret = ring_buf_put(socket_data->buf_pool, socket_data->receive_buf.buf,
socket_data->parser_start_index_eof);
if (ret <= 0) {
LOG_ERR("ring_buf_put failed: %d", ret);
return 0;
}
/* Mark that payload was successfully pushed and EOF was detected */
socket_data->parser_socket_data_received = true;
socket_data->parser_eof_detected = true;
LOG_DBG("pushed %d bytes to ring_buf; "
"collected_buf_len(before)=%u",
ret, socket_data->collected_buf_len);
socket_data->collected_buf_len += ret;
LOG_DBG("parser_socket_data_received=1 "
"collected_buf_len(after)=%u",
socket_data->collected_buf_len);
return ret;
}
return 0;
}
/* Helper: centralize handling when the chat end-delimiter has been fully
* received. Returns true if caller should return immediately after handling.
*/
static bool handle_delimiter_complete(struct hl78xx_socket_data *socket_data,
struct modem_chat *chat)
{
if (!modem_chat_parse_end_del_complete(socket_data, chat)) {
return false;
}
if (is_end_delimiter_only(socket_data)) {
parser_reset(socket_data);
return true;
}
socket_data->parser_size_of_socketdata = socket_data->receive_buf.len;
if (socket_data->parser_state == HL78XX_PARSER_CONNECT_MATCHED &&
socket_data->parser_state != HL78XX_PARSER_EOF_OK_MATCHED) {
size_t connect_len = strlen(CONNECT_STRING);
size_t connect_plus_delim = connect_len + chat->delimiter_size;
/* Case 1: Drop the initial "CONNECT" line including its CRLF */
if (socket_data->receive_buf.len == connect_plus_delim &&
modem_chat_match_matches_received(socket_data, CONNECT_STRING,
(uint16_t)connect_len)) {
parser_reset(socket_data);
return true;
}
/* Case 2: Try to handle EOF; only reset if EOF was actually found/pushed */
if (handle_eof_pattern(socket_data) > 0) {
parser_reset(socket_data);
return true;
}
/* Not the initial CONNECT+CRLF and no EOF yet -> keep accumulating */
return false;
}
/* For other states, treat CRLF as end-of-line and reset as before */
parser_reset(socket_data);
return true;
}
/* Convenience helper for matching an exact string against the receive buffer.
* This consolidates the repeated pattern of checking length and content.
*/
static inline bool modem_chat_match_exact(struct hl78xx_socket_data *socket_data, const char *match)
{
size_t size_match = strlen(match);
if (socket_data->receive_buf.len != size_match) {
return false;
}
return modem_chat_match_matches_received(socket_data, match, (uint16_t)size_match);
}
static void socket_process_bytes(struct hl78xx_socket_data *socket_data, char byte)
{
const size_t cme_size = strlen(CME_ERROR_STRING);
if (is_receive_buffer_full(socket_data)) {
LOG_WRN("Receive buffer overrun");
parser_reset(socket_data);
return;
}
socket_data->receive_buf.buf[socket_data->receive_buf.len++] = byte;
handle_expected_length_decrement(socket_data);
if (handle_delimiter_complete(socket_data, &socket_data->mdata_global->chat)) {
return;
}
if (modem_chat_parse_end_del_start(socket_data, &socket_data->mdata_global->chat)) {
return;
}
if (socket_data->parser_state != HL78XX_PARSER_ERROR_MATCHED &&
socket_data->parser_state != HL78XX_PARSER_CONNECT_MATCHED) {
/* Exact CONNECT match: length must equal CONNECT string length */
if (modem_chat_match_exact(socket_data, CONNECT_STRING)) {
socket_data->parser_state = HL78XX_PARSER_CONNECT_MATCHED;
LOG_DBG("CONNECT matched. Expecting %d more bytes.",
socket_data->expected_buf_len);
return;
}
/* Partial CME ERROR match: length must be at least CME string length */
if (socket_data->receive_buf.len >= cme_size &&
modem_chat_match_matches_received(socket_data, CME_ERROR_STRING,
(uint16_t)cme_size)) {
socket_data->parser_state =
HL78XX_PARSER_ERROR_MATCHED; /* prevent further parsing */
LOG_ERR("CME ERROR received. Connection failed.");
socket_data->expected_buf_len = 0;
socket_data->collected_buf_len = 0;
parser_reset(socket_data);
socket_data->socket_data_error = true;
k_sem_give(&socket_data->mdata_global->script_stopped_sem_rx_int);
return;
}
}
if (socket_data->parser_state == HL78XX_PARSER_CONNECT_MATCHED &&
socket_data->parser_state != HL78XX_PARSER_EOF_OK_MATCHED &&
modem_chat_match_exact(socket_data, OK_STRING)) {
socket_data->parser_state = HL78XX_PARSER_EOF_OK_MATCHED;
/* Mark that OK was observed. Payload may have already been pushed by EOF handler.
*/
socket_data->parser_ok_detected = true;
LOG_DBG("OK matched. parser_ok_detected=%d parser_socket_data_received=%d "
"collected=%u",
socket_data->parser_ok_detected, socket_data->parser_socket_data_received,
socket_data->collected_buf_len);
}
}
/* ===== Modem pipe handlers ===========================================
* Handlers and callbacks for modem pipe events (receive/transmit).
*/
static int modem_process_handler(struct hl78xx_data *data)
{
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
char work_buf_local[HL78XX_UART_PIPE_WORK_SOCKET_BUFFER_SIZE] = {0};
int recv_len = 0;
int work_len = 0;
/* If no more data is expected, set leftover state and return */
if (socket_data->expected_buf_len == 0) {
LOG_DBG("No more data expected");
atomic_set_bit(&socket_data->mdata_global->state_leftover,
MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT);
return 0;
}
/* Use a small stack buffer for the pipe read to avoid TU-global BSS */
work_len = MIN(sizeof(work_buf_local), socket_data->expected_buf_len);
recv_len =
modem_pipe_receive(socket_data->mdata_global->uart_pipe, work_buf_local, work_len);
if (recv_len <= 0) {
return recv_len;
}
#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG
LOG_HEXDUMP_DBG(work_buf_local, recv_len, "Received bytes:");
#endif /* CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG */
for (int i = 0; i < recv_len; i++) {
socket_process_bytes(socket_data, work_buf_local[i]);
}
LOG_DBG("post-process state=%d recv_len=%d recv_buf.len=%u "
"expected=%u collected=%u socket_data_received=%d",
socket_data->parser_state, recv_len, socket_data->receive_buf.len,
socket_data->expected_buf_len, socket_data->collected_buf_len,
socket_data->parser_socket_data_received);
/* Check if we've completed reception */
if (socket_data->parser_eof_detected && socket_data->parser_ok_detected &&
socket_data->parser_socket_data_received) {
LOG_DBG("All data received: %d bytes", socket_data->parser_size_of_socketdata);
socket_data->expected_buf_len = 0;
LOG_DBG("About to give RX semaphore (eof=%d ok=%d socket_data_received=%d "
"collected=%u)",
socket_data->parser_eof_detected, socket_data->parser_ok_detected,
socket_data->parser_socket_data_received, socket_data->collected_buf_len);
k_sem_give(&socket_data->mdata_global->script_stopped_sem_rx_int);
/* Clear parser progress after the receiver has been notified */
found_reset(socket_data);
}
return 0;
}
static void modem_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event,
void *user_data)
{
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
switch (event) {
case MODEM_PIPE_EVENT_RECEIVE_READY:
(void)modem_process_handler(data);
break;
case MODEM_PIPE_EVENT_TRANSMIT_IDLE:
k_sem_give(&data->script_stopped_sem_tx_int);
break;
default:
LOG_DBG("Unhandled event: %d", event);
break;
}
}
void notif_carrier_off(const struct device *dev)
{
struct hl78xx_data *data = dev->data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
net_if_carrier_off(socket_data->net_iface);
}
void notif_carrier_on(const struct device *dev)
{
struct hl78xx_data *data = dev->data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
net_if_carrier_on(socket_data->net_iface);
}
void iface_status_work_cb(struct hl78xx_data *data, modem_chat_script_callback script_user_callback)
{
const char *cmd = "AT+CGCONTRDP=1";
int ret = 0;
ret = modem_dynamic_cmd_send(data, script_user_callback, cmd, strlen(cmd),
hl78xx_get_cgdcontrdp_match(), 1, false);
if (ret < 0) {
LOG_ERR("Failed to send AT+CGCONTRDP command: %d", ret);
return;
}
}
void dns_work_cb(const struct device *dev, bool hard_reset)
{
#if defined(CONFIG_DNS_RESOLVER) && !defined(CONFIG_DNS_SERVER_IP_ADDRESSES)
int ret;
struct hl78xx_data *data = dev->data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
struct dns_resolve_context *dnsCtx;
struct sockaddr temp_addr;
bool valid_address = false;
bool retry = false;
const char *const dns_servers_str[DNS_SERVERS_COUNT] = {
#ifdef CONFIG_NET_IPV6
socket_data->dns.v6_string,
#endif
#ifdef CONFIG_NET_IPV4
socket_data->dns.v4_string,
#endif
NULL};
const char *dns_servers_wrapped[ARRAY_SIZE(dns_servers_str)];
if (hard_reset) {
LOG_DBG("Resetting DNS resolver");
dnsCtx = dns_resolve_get_default();
if (!dnsCtx) {
LOG_WRN("No default DNS resolver context available; skipping "
"reconfigure");
socket_data->dns.ready = true;
return;
}
if (dnsCtx->state != DNS_RESOLVE_CONTEXT_INACTIVE) {
dns_resolve_close(dnsCtx);
}
socket_data->dns.ready = false;
}
#ifdef CONFIG_NET_IPV6
valid_address = net_ipaddr_parse(socket_data->dns.v6_string,
strlen(socket_data->dns.v6_string), &temp_addr);
if (!valid_address && IS_ENABLED(CONFIG_NET_IPV4)) {
/* IPv6 DNS string is not valid, replace it with IPv4 address and recheck */
#ifdef CONFIG_NET_IPV4
strncpy(socket_data->dns.v6_string, socket_data->dns.v4_string,
sizeof(socket_data->dns.v4_string) - 1);
valid_address = net_ipaddr_parse(socket_data->dns.v6_string,
strlen(socket_data->dns.v6_string), &temp_addr);
#endif
}
#elif defined(CONFIG_NET_IPV4)
valid_address = net_ipaddr_parse(socket_data->dns.v4_string,
strlen(socket_data->dns.v4_string), &temp_addr);
#else
/* No IP stack configured */
valid_address = false;
#endif
if (!valid_address) {
LOG_WRN("No valid DNS address!");
return;
}
if (!socket_data->net_iface || !net_if_is_up(socket_data->net_iface) ||
socket_data->dns.ready) {
LOG_DBG("DNS already ready or net_iface problem %d %d %d", !socket_data->net_iface,
!net_if_is_up(socket_data->net_iface), socket_data->dns.ready);
return;
}
memcpy(dns_servers_wrapped, dns_servers_str, sizeof(dns_servers_wrapped));
/* set new DNS addr in DNS resolver */
LOG_DBG("Refresh DNS resolver");
dnsCtx = dns_resolve_get_default();
ret = dns_resolve_reconfigure(dnsCtx, dns_servers_wrapped, NULL, DNS_SOURCE_MANUAL);
if (ret < 0) {
LOG_ERR("dns_resolve_reconfigure fail (%d)", ret);
retry = true;
} else {
LOG_DBG("DNS ready");
socket_data->dns.ready = true;
}
if (retry) {
LOG_WRN("DNS not ready, scheduling a retry");
}
#endif
}
static int on_cmd_sockread_common(int socket_id, uint16_t socket_data_length, uint16_t len,
void *user_data)
{
struct modem_socket *sock;
struct socket_read_data *sock_data;
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
int ret = 0;
sock = modem_socket_from_fd(&socket_data->socket_config, socket_id);
if (!sock) {
LOG_ERR("Socket not found! (%d)", socket_id);
return -EINVAL;
}
sock_data = sock->data;
if (!sock_data) {
LOG_ERR("Socket data missing! Ignoring (%d)", socket_id);
return -EINVAL;
}
if (socket_data->socket_data_error && socket_data->collected_buf_len == 0) {
errno = ECONNABORTED;
return -ECONNABORTED;
}
if ((len <= 0) || socket_data_length <= 0 || socket_data->collected_buf_len < (size_t)len) {
LOG_ERR("%d Invalid data length: %d %d %d Aborting!", __LINE__, socket_data_length,
(int)len, socket_data->collected_buf_len);
return -EAGAIN;
}
if (len < socket_data_length) {
LOG_DBG("Incomplete data received! Expected: %d, Received: %d", socket_data_length,
len);
return -EAGAIN;
}
ret = ring_buf_get(socket_data->buf_pool, sock_data->recv_buf, len);
if (ret != len) {
LOG_ERR("%d Data retrieval mismatch: expected %u, got %d", __LINE__, len, ret);
return -EAGAIN;
}
#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG
LOG_HEXDUMP_DBG(sock_data->recv_buf, ret, "Received Data:");
#endif /* CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG */
if (sock_data->recv_buf_len < (size_t)len) {
LOG_ERR("Buffer overflow! Received: %zu vs. Available: %zu", len,
sock_data->recv_buf_len);
return -EINVAL;
}
if ((size_t)len != (size_t)socket_data_length) {
LOG_ERR("Data mismatch! Copied: %zu vs. Received: %d", len, socket_data_length);
return -EINVAL;
}
sock_data->recv_read_len = len;
/* Remove packet from list */
modem_socket_next_packet_size(&socket_data->socket_config, sock);
modem_socket_packet_size_update(&socket_data->socket_config, sock, -socket_data_length);
socket_data->collected_buf_len = 0;
return len;
}
int modem_handle_data_capture(size_t target_len, struct hl78xx_data *data)
{
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
return on_cmd_sockread_common(socket_data->current_sock_fd, socket_data->sizeof_socket_data,
target_len, data);
}
static int extract_ip_family_and_port(const struct sockaddr *addr, int *af, uint16_t *port)
{
#if defined(CONFIG_NET_IPV6)
if (addr->sa_family == AF_INET6) {
*port = ntohs(net_sin6(addr)->sin6_port);
*af = MDM_HL78XX_SOCKET_AF_IPV6;
} else {
#endif /* CONFIG_NET_IPV6 */
#if defined(CONFIG_NET_IPV4)
if (addr->sa_family == AF_INET) {
*port = ntohs(net_sin(addr)->sin_port);
*af = MDM_HL78XX_SOCKET_AF_IPV4;
} else {
#endif /* CONFIG_NET_IPV4 */
errno = EAFNOSUPPORT;
return -1;
#if defined(CONFIG_NET_IPV4)
}
#endif /* CONFIG_NET_IPV4 */
#if defined(CONFIG_NET_IPV6)
}
#endif /* CONFIG_NET_IPV6 */
return 0;
}
static int format_ip_and_setup_tls(struct hl78xx_socket_data *socket_data,
const struct sockaddr *addr, char *ip_str, size_t ip_str_len,
struct modem_socket *sock)
{
int ret = modem_context_sprint_ip_addr(addr, ip_str, ip_str_len);
if (ret != 0) {
LOG_ERR("Failed to format IP!");
errno = ENOMEM;
return -1;
}
if (sock->ip_proto == IPPROTO_TCP) {
/* Determine actual length of the formatted IP string (it may be
* shorter than the provided buffer size). Copy at most
* MDM_MAX_HOSTNAME_LEN - 1 bytes and ensure NUL-termination to
* avoid writing past the hostname buffer.
*/
size_t actual_len = strnlen(ip_str, ip_str_len);
size_t copy_len = MIN(actual_len, (size_t)MDM_MAX_HOSTNAME_LEN - 1);
if (copy_len > 0) {
memcpy(socket_data->tls.hostname, ip_str, copy_len);
}
socket_data->tls.hostname[copy_len] = '\0';
socket_data->tls.hostname_set = false;
}
return 0;
}
static int send_tcp_or_tls_config(struct modem_socket *sock, uint16_t dst_port, int af, int mode,
struct hl78xx_socket_data *socket_data)
{
int ret = 0;
char cmd_buf[sizeof("AT+KTCPCFG=#,#,\"" MODEM_HL78XX_ADDRESS_FAMILY_FORMAT
"\",#####,,,,#,,#") +
MDM_MAX_HOSTNAME_LEN + NET_IPV6_ADDR_LEN];
snprintk(cmd_buf, sizeof(cmd_buf), "AT+KTCPCFG=1,%d,\"%s\",%u,,,,%d,%s,0", mode,
socket_data->tls.hostname, dst_port, af, mode == 3 ? "0" : "");
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf),
hl78xx_get_ktcpcfg_match(), 1, false);
if (ret < 0 ||
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].is_created == false) {
LOG_ERR("%s ret:%d", cmd_buf, ret);
modem_socket_put(&socket_data->socket_config, sock->sock_fd);
/* Map negative internal return codes to positive errno; fall back to EIO
* when the code is non-negative but the operation failed.
*/
hl78xx_set_errno_from_code(ret);
return -1;
}
return 0;
}
static int send_udp_config(const struct sockaddr *addr, struct hl78xx_socket_data *socket_data,
struct modem_socket *sock)
{
int ret = 0;
char cmd_buf[64];
uint8_t display_data_urc = 0;
#if defined(CONFIG_MODEM_HL78XX_SOCKET_UDP_DISPLAY_DATA_URC)
display_data_urc = CONFIG_MODEM_HL78XX_SOCKET_UDP_DISPLAY_DATA_URC;
#endif
snprintk(cmd_buf, sizeof(cmd_buf), "AT+KUDPCFG=1,%u,,%d,,,%d,%d", 0, display_data_urc,
(addr->sa_family - 1), 0);
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf),
hl78xx_get_kudpind_match(), 1, false);
if (ret < 0) {
goto error;
}
return 0;
error:
LOG_ERR("%s ret:%d", cmd_buf, ret);
modem_socket_put(&socket_data->socket_config, sock->sock_fd);
hl78xx_set_errno_from_code(ret);
return -1;
}
static int create_socket(struct modem_socket *sock, const struct sockaddr *addr,
struct hl78xx_socket_data *data)
{
LOG_DBG("entry fd=%d id=%d", sock->sock_fd, sock->id);
int af;
uint16_t dst_port;
char ip_str[NET_IPV6_ADDR_LEN];
bool is_udp;
int mode;
int ret;
/* save destination address */
memcpy(&sock->dst, addr, sizeof(*addr));
if (extract_ip_family_and_port(addr, &af, &dst_port) < 0) {
return -1;
}
if (format_ip_and_setup_tls(data, addr, ip_str, sizeof(ip_str), sock) < 0) {
return -1;
}
is_udp = (sock->ip_proto == IPPROTO_UDP);
if (is_udp) {
ret = send_udp_config(addr, data, sock);
LOG_DBG("send_udp_config returned %d", ret);
return ret;
}
mode = (sock->ip_proto == IPPROTO_TLS_1_2) ? 3 : 0;
/* only TCP and TLS are supported */
if (sock->ip_proto != IPPROTO_TCP && sock->ip_proto != IPPROTO_TLS_1_2) {
LOG_ERR("Unsupported protocol: %d", sock->ip_proto);
errno = EPROTONOSUPPORT;
return -1;
}
LOG_DBG("TCP/TLS socket, calling send_tcp_or_tls_config af=%d port=%u "
"mode=%d",
af, dst_port, mode);
ret = send_tcp_or_tls_config(sock, dst_port, af, mode, data);
LOG_DBG("send_tcp_or_tls_config returned %d", ret);
return ret;
}
static int socket_close(struct hl78xx_socket_data *socket_data, struct modem_socket *sock)
{
char buf[sizeof("AT+KTCPCLOSE=##\r")];
int ret = 0;
if (sock->ip_proto == IPPROTO_UDP) {
snprintk(buf, sizeof(buf), "AT+KUDPCLOSE=%d", sock->id);
} else {
snprintk(buf, sizeof(buf), "AT+KTCPCLOSE=%d", sock->id);
}
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, buf, strlen(buf),
hl78xx_get_sockets_allow_matches(),
hl78xx_get_sockets_allow_matches_size(), false);
if (ret < 0) {
LOG_ERR("%s ret:%d", buf, ret);
}
return ret;
}
static int socket_delete(struct hl78xx_socket_data *socket_data, struct modem_socket *sock)
{
char buf[sizeof("AT+KTCPDEL=##\r")];
int ret = 0;
if (sock->ip_proto == IPPROTO_UDP) {
/**
* snprintk(buf, sizeof(buf), "AT+KUDPDEL=%d", sock->id);
* No need to delete udp config here according to ref guide. The at UDPCLOSE
* automatically deletes the session
*/
return 0;
}
snprintk(buf, sizeof(buf), "AT+KTCPDEL=%d", sock->id);
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, buf, strlen(buf),
hl78xx_get_sockets_allow_matches(),
hl78xx_get_sockets_allow_matches_size(), false);
if (ret < 0) {
LOG_ERR("%s ret:%d", buf, ret);
}
return ret;
}
/* ===== Socket Offload OPS ======================================== */
static int offload_socket(int family, int type, int proto)
{
int ret;
/* defer modem's socket create call to bind(); use accessor and check */
struct hl78xx_socket_data *g = hl78xx_get_socket_global();
HL78XX_LOG_DBG("%d %d %d %d", __LINE__, family, type, proto);
if (!g) {
LOG_ERR("Socket global not initialized");
errno = ENODEV;
return -1;
}
ret = modem_socket_get(&g->socket_config, family, type, proto);
if (ret < 0) {
hl78xx_set_errno_from_code(ret);
return -1;
}
errno = 0;
return ret;
}
static int offload_close(void *obj)
{
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = NULL;
if (!sock) {
return -EINVAL;
}
/* Recover the containing instance; guard in case sock isn't from this driver */
socket_data = hl78xx_get_socket_global();
if (!socket_data || !socket_data->offload_dev ||
socket_data->offload_dev->data != socket_data) {
LOG_WRN("parent mismatch: parent != offload_dev->data (%p != %p)", socket_data,
socket_data ? socket_data->offload_dev->data : NULL);
errno = EINVAL;
return -1;
}
/* make sure socket is allocated and assigned an id */
if (modem_socket_id_is_assigned(&socket_data->socket_config, sock) == false) {
return 0;
}
if (validate_socket(sock, socket_data) == 0) {
socket_close(socket_data, sock);
socket_delete(socket_data, sock);
modem_socket_put(&socket_data->socket_config, sock->sock_fd);
sock->is_connected = false;
}
/* Consider here successfully socket is closed */
return 0;
}
static int offload_bind(void *obj, const struct sockaddr *addr, socklen_t addrlen)
{
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
int ret = 0;
if (!sock || !socket_data || !socket_data->offload_dev) {
errno = EINVAL;
return -1;
}
LOG_DBG("entry for socket fd=%d id=%d", ((struct modem_socket *)obj)->sock_fd,
((struct modem_socket *)obj)->id);
/* Save bind address information */
memcpy(&sock->src, addr, sizeof(*addr));
/* Check if socket is allocated */
if (modem_socket_is_allocated(&socket_data->socket_config, sock)) {
/* Trigger socket creation */
ret = create_socket(sock, addr, socket_data);
LOG_DBG("create_socket returned %d", ret);
if (ret < 0) {
LOG_ERR("%d %s SOCKET CREATION", __LINE__, __func__);
return -1;
}
}
return 0;
}
static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t addrlen)
{
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
int ret = 0;
char cmd_buf[sizeof("AT+KTCPCFG=#\r")];
char ip_str[NET_IPV6_ADDR_LEN];
if (!addr || !socket_data || !socket_data->offload_dev) {
errno = EINVAL;
return -1;
}
if (!hl78xx_is_registered(socket_data->mdata_global)) {
errno = ENETUNREACH;
return -1;
}
/* make sure socket has been allocated */
if (modem_socket_is_allocated(&socket_data->socket_config, sock) == false) {
LOG_ERR("Invalid socket_id(%d) from fd:%d", sock->id, sock->sock_fd);
errno = EINVAL;
return -1;
}
/* make sure we've created the socket */
if (modem_socket_id_is_assigned(&socket_data->socket_config, sock) == false) {
LOG_DBG("%d no socket assigned", __LINE__);
if (create_socket(sock, addr, socket_data) < 0) {
return -1;
}
}
memcpy(&sock->dst, addr, sizeof(*addr));
/* skip socket connect if UDP */
if (sock->ip_proto == IPPROTO_UDP) {
errno = 0;
return 0;
}
ret = modem_context_sprint_ip_addr(addr, ip_str, sizeof(ip_str));
if (ret != 0) {
hl78xx_set_errno_from_code(ret);
LOG_ERR("Error formatting IP string %d", ret);
return -1;
}
/* send connect command */
snprintk(cmd_buf, sizeof(cmd_buf), "AT+KTCPCNX=%d", sock->id);
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf),
hl78xx_get_ktcpind_match(), 1, false);
if (ret < 0 ||
socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].is_connected == false) {
sock->is_connected = false;
LOG_ERR("%s ret:%d", cmd_buf, ret);
/* Map tcp_conn_status.err_code:
* - positive values are assumed to be direct POSIX errno values -> pass
* through
* - zero or unknown -> use conservative EIO
*/
errno = (socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].err_code > 0)
? socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)]
.err_code
: EIO;
return -1;
}
sock->is_connected = true;
errno = 0;
return 0;
}
static bool validate_recv_args(void *buf, size_t len, int flags)
{
if (!buf || len == 0) {
errno = EINVAL;
return false;
}
if (flags & ZSOCK_MSG_PEEK) {
errno = ENOTSUP;
return false;
}
return true;
}
static int wait_for_data_if_needed(struct hl78xx_socket_data *socket_data,
struct modem_socket *sock, int flags)
{
int size = modem_socket_next_packet_size(&socket_data->socket_config, sock);
if (size > 0) {
return size;
}
if (flags & ZSOCK_MSG_DONTWAIT) {
errno = EAGAIN;
return -1;
}
if (validate_socket(sock, socket_data) == -1) {
errno = 0;
return 0;
}
modem_socket_wait_data(&socket_data->socket_config, sock);
return modem_socket_next_packet_size(&socket_data->socket_config, sock);
}
static void prepare_read_command(struct hl78xx_socket_data *socket_data, char *sendbuf,
size_t bufsize, struct modem_socket *sock, size_t read_size)
{
snprintk(sendbuf, bufsize, "AT+K%sRCV=%d,%zd%s",
sock->ip_proto == IPPROTO_UDP ? "UDP" : "TCP", sock->id, read_size,
socket_data->mdata_global->chat.delimiter);
}
/* Perform the receive transaction: release chat, attach pipe, wait for tx sem,
* transmit read command, wait for rx sem and capture data. Returns 0 on
* success or a negative code which will be mapped by caller.
*/
static int hl78xx_perform_receive_transaction(struct hl78xx_socket_data *socket_data,
const char *sendbuf)
{
int rv;
int ret;
modem_chat_release(&socket_data->mdata_global->chat);
modem_pipe_attach(socket_data->mdata_global->uart_pipe, modem_pipe_callback,
socket_data->mdata_global);
rv = k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER);
if (rv < 0) {
LOG_ERR("%s: k_sem_take(tx) returned %d", __func__, rv);
return rv;
}
ret = modem_pipe_transmit(socket_data->mdata_global->uart_pipe, (const uint8_t *)sendbuf,
strlen(sendbuf));
if (ret < 0) {
LOG_ERR("Error sending read command: %d", ret);
return ret;
}
rv = k_sem_take(&socket_data->mdata_global->script_stopped_sem_rx_int, K_FOREVER);
if (rv < 0) {
return rv;
}
rv = modem_handle_data_capture(socket_data->sizeof_socket_data, socket_data->mdata_global);
if (rv < 0) {
return rv;
}
return 0;
}
static void setup_socket_data(struct hl78xx_socket_data *socket_data, struct modem_socket *sock,
struct socket_read_data *sock_data, void *buf, size_t len,
struct sockaddr *from, uint16_t read_size)
{
memset(sock_data, 0, sizeof(*sock_data));
sock_data->recv_buf = buf;
sock_data->recv_buf_len = len;
sock_data->recv_addr = from;
sock->data = sock_data;
socket_data->sizeof_socket_data = read_size;
socket_data->requested_socket_id = sock->id;
socket_data->current_sock_fd = sock->sock_fd;
socket_data->expected_buf_len = read_size + sizeof("\r\n") - 1 +
socket_data->mdata_global->buffers.eof_pattern_size +
sizeof(MODEM_STREAM_END_WORD) - 1;
socket_data->collected_buf_len = 0;
socket_data->socket_data_error = false;
}
static void check_tcp_state_if_needed(struct hl78xx_socket_data *socket_data,
struct modem_socket *sock)
{
const char *check_ktcp_stat = "AT+KTCPSTAT";
/* Only check for TCP sockets */
if (sock->type != SOCK_STREAM) {
return;
}
if (atomic_test_and_clear_bit(&socket_data->mdata_global->state_leftover,
MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT) &&
sock && sock->ip_proto == IPPROTO_TCP) {
modem_dynamic_cmd_send(socket_data->mdata_global, NULL, check_ktcp_stat,
strlen(check_ktcp_stat), hl78xx_get_ktcp_state_match(), 1,
true);
}
}
static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, struct sockaddr *from,
socklen_t *fromlen)
{
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
char sendbuf[sizeof("AT+KUDPRCV=#,##########\r\n")];
struct socket_read_data sock_data;
int next_packet_size = 0;
uint32_t max_data_length = 0;
uint16_t read_size = 0;
int trv = 0;
int ret;
if (!sock || !socket_data || !socket_data->offload_dev) {
errno = EINVAL;
return -1;
}
/* If modem is not registered yet, propagate EAGAIN to indicate try again
* later. However, if the socket simply isn't connected (validate_socket
* returns -1) we return 0 with errno cleared so upper layers (eg. DNS
* dispatcher) treat this as no data available rather than an error and
* avoid noisy repeated error logs.
*/
if (!hl78xx_is_registered(socket_data->mdata_global)) {
errno = EAGAIN;
return -1;
}
if (validate_socket(sock, socket_data) == -1) {
errno = 0;
return 0;
}
if (!validate_recv_args(buf, len, flags)) {
return -1;
}
ret = k_mutex_lock(&socket_data->mdata_global->tx_lock, K_SECONDS(1));
if (ret < 0) {
LOG_ERR("Failed to acquire TX lock: %d", ret);
hl78xx_set_errno_from_code(ret);
return -1;
}
next_packet_size = wait_for_data_if_needed(socket_data, sock, flags);
if (next_packet_size <= 0) {
ret = next_packet_size;
goto exit;
}
max_data_length =
MDM_MAX_DATA_LENGTH - (socket_data->mdata_global->buffers.eof_pattern_size +
sizeof(MODEM_STREAM_STARTER_WORD) - 1);
/* limit read size to modem max data length */
next_packet_size = MIN(next_packet_size, max_data_length);
/* limit read size to user buffer length */
read_size = MIN(next_packet_size, len);
/* prepare socket data for the read operation */
setup_socket_data(socket_data, sock, &sock_data, buf, len, from, read_size);
prepare_read_command(socket_data, sendbuf, sizeof(sendbuf), sock, read_size);
HL78XX_LOG_DBG("%d socket_fd: %d, socket_id: %d, expected_data_len: %d", __LINE__,
socket_data->current_sock_fd, socket_data->requested_socket_id,
socket_data->expected_buf_len);
LOG_HEXDUMP_DBG(sendbuf, strlen(sendbuf), "sending");
trv = hl78xx_perform_receive_transaction(socket_data, sendbuf);
if (trv < 0) {
hl78xx_set_errno_from_code(trv);
ret = -1;
goto exit;
}
if (from && fromlen) {
*fromlen = sizeof(sock->dst);
memcpy(from, &sock->dst, *fromlen);
}
errno = 0;
ret = sock_data.recv_read_len;
exit:
k_mutex_unlock(&socket_data->mdata_global->tx_lock);
modem_chat_attach(&socket_data->mdata_global->chat, socket_data->mdata_global->uart_pipe);
socket_data->expected_buf_len = 0;
check_tcp_state_if_needed(socket_data, sock);
return ret;
}
int check_if_any_socket_connected(const struct device *dev)
{
struct hl78xx_data *data = dev->data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
struct modem_socket_config *cfg = &socket_data->socket_config;
k_sem_take(&cfg->sem_lock, K_FOREVER);
for (int i = 0; i < cfg->sockets_len; i++) {
if (cfg->sockets[i].is_connected) {
/* if there is any socket connected */
k_sem_give(&cfg->sem_lock);
return true;
}
}
k_sem_give(&cfg->sem_lock);
return false;
}
/* ===== Send / Receive helpers ========================================
* Helpers used by sendto/recv paths, preparing commands and transmitting
* data over the modem pipe.
*/
static int prepare_send_cmd(const struct modem_socket *sock, const struct sockaddr *dst_addr,
size_t buf_len, char *cmd_buf, size_t cmd_buf_size)
{
int ret = 0;
if (sock->ip_proto == IPPROTO_UDP) {
char ip_str[NET_IPV6_ADDR_LEN];
uint16_t dst_port = 0;
ret = modem_context_sprint_ip_addr(dst_addr, ip_str, sizeof(ip_str));
if (ret < 0) {
LOG_ERR("Error formatting IP string %d", ret);
return ret;
}
ret = modem_context_get_addr_port(dst_addr, &dst_port);
if (ret < 0) {
LOG_ERR("Error getting port from IP address %d", ret);
return ret;
}
snprintk(cmd_buf, cmd_buf_size, "AT+KUDPSND=%d,\"%s\",%u,%zu", sock->id, ip_str,
dst_port, buf_len);
return 0;
}
/* Default to TCP-style send command */
snprintk(cmd_buf, cmd_buf_size, "AT+KTCPSND=%d,%zu", sock->id, buf_len);
return 0;
}
static int send_data_buffer(struct hl78xx_socket_data *socket_data, const char *buf,
const size_t buf_len, int *sock_written)
{
uint32_t offset = 0;
int len = buf_len;
int ret = 0;
if (len == 0) {
LOG_DBG("%d No data to send", __LINE__);
return 0;
}
while (len > 0) {
LOG_DBG("waiting for TX semaphore (offset=%u len=%d)", offset, len);
if (k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER) <
0) {
LOG_ERR("%s: k_sem_take(tx) failed", __func__);
return -1;
}
ret = modem_pipe_transmit(socket_data->mdata_global->uart_pipe,
((const uint8_t *)buf) + offset, len);
if (ret <= 0) {
LOG_ERR("Transmit error %d", ret);
return -1;
}
offset += ret;
len -= ret;
*sock_written += ret;
}
return 0;
}
static int validate_and_prepare(struct modem_socket *sock, const struct sockaddr **dst_addr,
size_t *buf_len, char *cmd_buf, size_t cmd_buf_len)
{
/* Validate args and prepare send command */
if (!sock) {
errno = EINVAL;
return -1;
}
if (sock->type != SOCK_DGRAM && !sock->is_connected) {
errno = ENOTCONN;
return -1;
}
if (!*dst_addr && sock->ip_proto == IPPROTO_UDP) {
*dst_addr = &sock->dst;
}
if (*buf_len > MDM_MAX_DATA_LENGTH) {
if (sock->type == SOCK_DGRAM) {
errno = EMSGSIZE;
return -1;
}
*buf_len = MDM_MAX_DATA_LENGTH;
}
/* Consolidated send command helper handles UDP vs TCP formatting */
return prepare_send_cmd(sock, *dst_addr, *buf_len, cmd_buf, cmd_buf_len);
}
static int transmit_regular_data(struct hl78xx_socket_data *socket_data, const char *buf,
size_t buf_len, int *sock_written)
{
int ret;
ret = send_data_buffer(socket_data, buf, buf_len, sock_written);
if (ret < 0) {
return ret;
}
ret = k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER);
if (ret < 0) {
LOG_ERR("%s: k_sem_take(tx) returned %d", __func__, ret);
return ret;
}
return modem_pipe_transmit(socket_data->mdata_global->uart_pipe,
(uint8_t *)socket_data->mdata_global->buffers.eof_pattern,
socket_data->mdata_global->buffers.eof_pattern_size);
}
/* send binary data via the +KUDPSND/+KTCPSND commands */
static ssize_t send_socket_data(void *obj, struct hl78xx_socket_data *socket_data,
const struct sockaddr *dst_addr, const char *buf, size_t buf_len,
k_timeout_t timeout)
{
struct modem_socket *sock = (struct modem_socket *)obj;
char cmd_buf[82] = {0}; /* AT+KUDPSND/KTCP=,IP,PORT,LENGTH */
int ret;
int sock_written = 0;
ret = validate_and_prepare(sock, &dst_addr, &buf_len, cmd_buf, sizeof(cmd_buf));
if (ret < 0) {
return ret;
}
socket_data->socket_data_error = false;
if (k_mutex_lock(&socket_data->mdata_global->tx_lock, K_SECONDS(1)) < 0) {
return -1;
}
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf),
(const struct modem_chat_match *)hl78xx_get_connect_matches(),
hl78xx_get_connect_matches_size(), false);
if (ret < 0 || socket_data->socket_data_error) {
hl78xx_set_errno_from_code(ret);
ret = -1;
goto cleanup;
}
modem_pipe_attach(socket_data->mdata_global->chat.pipe, modem_pipe_callback,
socket_data->mdata_global);
ret = transmit_regular_data(socket_data, buf, buf_len, &sock_written);
if (ret < 0) {
goto cleanup;
}
modem_chat_attach(&socket_data->mdata_global->chat, socket_data->mdata_global->uart_pipe);
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, "", 0,
hl78xx_get_sockets_ok_match(), 1, false);
if (ret < 0) {
LOG_ERR("Final confirmation failed: %d", ret);
goto cleanup;
}
cleanup:
k_mutex_unlock(&socket_data->mdata_global->tx_lock);
return (ret < 0) ? -1 : sock_written;
}
#ifdef CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS
/* ===== TLS implementation (conditional) ================================
* TLS credential upload and chipper settings helper implementations.
*/
static int handle_tls_sockopts(void *obj, int optname, const void *optval, socklen_t optlen)
{
int ret;
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
if (!sock || !socket_data || !socket_data->offload_dev) {
return -EINVAL;
}
switch (optname) {
case TLS_SEC_TAG_LIST:
ret = map_credentials(socket_data, optval, optlen);
return ret;
case TLS_HOSTNAME:
if (optlen >= MDM_MAX_HOSTNAME_LEN) {
return -EINVAL;
}
memset(socket_data->tls.hostname, 0, MDM_MAX_HOSTNAME_LEN);
memcpy(socket_data->tls.hostname, optval, optlen);
socket_data->tls.hostname[optlen] = '\0';
socket_data->tls.hostname_set = true;
ret = hl78xx_configure_chipper_suit(socket_data);
if (ret < 0) {
LOG_ERR("Failed to configure chipper suit: %d", ret);
return ret;
}
LOG_DBG("TLS hostname set to: %s", socket_data->tls.hostname);
return 0;
case TLS_PEER_VERIFY:
if (*(const uint32_t *)optval != TLS_PEER_VERIFY_REQUIRED) {
LOG_WRN("Disabling peer verification is not supported");
}
return 0;
case TLS_CERT_NOCOPY:
return 0; /* No-op, success */
default:
LOG_DBG("Unsupported TLS option: %d", optname);
return -EINVAL;
}
}
static int offload_setsockopt(void *obj, int level, int optname, const void *optval,
socklen_t optlen)
{
int ret = 0;
if (!IS_ENABLED(CONFIG_NET_SOCKETS_SOCKOPT_TLS)) {
return -EINVAL;
}
if (level == SOL_TLS) {
ret = handle_tls_sockopts(obj, optname, optval, optlen);
if (ret < 0) {
hl78xx_set_errno_from_code(ret);
return -1;
}
return 0;
}
LOG_DBG("Unsupported socket option: %d", optname);
return -EINVAL;
}
#endif /* CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS */
static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags,
const struct sockaddr *to, socklen_t tolen)
{
int ret = 0;
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
if (!sock || !socket_data || !socket_data->offload_dev) {
errno = EINVAL;
return -1;
}
if (!hl78xx_is_registered(socket_data->mdata_global)) {
LOG_ERR("Modem currently not attached to the network!");
return -EAGAIN;
}
/* Do some sanity checks. */
if (!buf || len == 0) {
errno = EINVAL;
return -1;
}
/* For stream sockets (TCP) the socket must be connected. For datagram
* sockets (UDP) sendto can be used without a prior connect as long as a
* destination address is provided or the socket has a stored dst. The
* helper validate_and_prepare will supply sock->dst for UDP when needed.
*/
if (sock->type != SOCK_DGRAM && !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 = send_socket_data(obj, socket_data, to, buf, len, K_SECONDS(MDM_CMD_TIMEOUT));
if (ret < 0) {
/* Map internal negative return codes to positive errno values. Use EIO as
* a conservative fallback when ret is non-negative (unexpected)
*/
hl78xx_set_errno_from_code(ret);
return -1;
}
errno = 0;
return ret;
}
static int offload_ioctl(void *obj, unsigned int request, va_list args)
{
int ret = 0;
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
struct zsock_pollfd *pfd;
struct k_poll_event **pev;
struct k_poll_event *pev_end;
/* sanity check: does parent == parent->offload_dev->data ? */
if (socket_data && socket_data->offload_dev &&
socket_data->offload_dev->data != socket_data) {
LOG_WRN("parent mismatch: parent != offload_dev->data (%p != %p)", socket_data,
socket_data->offload_dev->data);
}
switch (request) {
case ZFD_IOCTL_POLL_PREPARE:
pfd = va_arg(args, struct zsock_pollfd *);
pev = va_arg(args, struct k_poll_event **);
pev_end = va_arg(args, struct k_poll_event *);
ret = modem_socket_poll_prepare(&socket_data->socket_config, obj, pfd, pev,
pev_end);
if (ret == -1 && errno == ENOTSUP && (pfd->events & ZSOCK_POLLOUT) &&
sock->ip_proto == IPPROTO_UDP) {
/* Not Implemented */
/*
* You can implement this later when needed
* For now, just ignore it
*/
errno = ENOTSUP;
ret = 0;
}
return ret;
case ZFD_IOCTL_POLL_UPDATE:
pfd = va_arg(args, struct zsock_pollfd *);
pev = va_arg(args, struct k_poll_event **);
return modem_socket_poll_update(obj, pfd, pev);
case F_GETFL:
return 0;
case F_SETFL: {
#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG
int flags = va_arg(args, int);
LOG_DBG("F_SETFL called with flags=0x%x", flags);
ARG_UNUSED(flags);
#endif
/* You can store flags if you want, but it's safe to just ignore them. */
return 0;
}
default:
errno = EINVAL;
return -1;
}
}
static ssize_t offload_read(void *obj, void *buffer, size_t count)
{
return offload_recvfrom(obj, buffer, count, 0, NULL, 0);
}
static ssize_t offload_write(void *obj, const void *buffer, size_t count)
{
return offload_sendto(obj, buffer, count, 0, NULL, 0);
}
static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags)
{
ssize_t sent = 0;
struct iovec bkp_iovec = {0};
struct msghdr crafted_msg = {.msg_name = msg->msg_name, .msg_namelen = msg->msg_namelen};
struct modem_socket *sock = (struct modem_socket *)obj;
struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock);
size_t full_len = 0;
int ret;
if (!sock || !socket_data || !socket_data->offload_dev) {
errno = EINVAL;
return -1;
}
/* Compute the full length to send and validate input */
for (int i = 0; i < msg->msg_iovlen; i++) {
if (!msg->msg_iov[i].iov_base || msg->msg_iov[i].iov_len == 0) {
errno = EINVAL;
return -1;
}
full_len += msg->msg_iov[i].iov_len;
}
while (full_len > sent) {
int removed = 0;
int i = 0;
int bkp_iovec_idx = -1;
crafted_msg.msg_iovlen = msg->msg_iovlen;
crafted_msg.msg_iov = &msg->msg_iov[0];
/* Adjust iovec to remove already sent bytes */
while (removed < sent) {
int to_remove = sent - removed;
if (to_remove >= msg->msg_iov[i].iov_len) {
crafted_msg.msg_iovlen -= 1;
crafted_msg.msg_iov = &msg->msg_iov[i + 1];
removed += msg->msg_iov[i].iov_len;
} else {
bkp_iovec_idx = i;
bkp_iovec = msg->msg_iov[i];
msg->msg_iov[i].iov_len -= to_remove;
msg->msg_iov[i].iov_base =
((uint8_t *)msg->msg_iov[i].iov_base) + to_remove;
removed += to_remove;
}
i++;
}
/* send_socket_data expects a buffer pointer and its byte length.
* crafted_msg.msg_iovlen is the number of iovec entries and is
* incorrect here (was causing sends of '2' bytes when two iovecs
* were present). Use the first iovec's iov_len for the byte length.
*/
ret = send_socket_data(obj, socket_data, crafted_msg.msg_name,
crafted_msg.msg_iov->iov_base, crafted_msg.msg_iov->iov_len,
K_SECONDS(MDM_CMD_TIMEOUT));
if (bkp_iovec_idx != -1) {
msg->msg_iov[bkp_iovec_idx] = bkp_iovec;
}
if (ret < 0) {
/* Map negative internal return code to positive errno; fall back to
* EIO
*/
hl78xx_set_errno_from_code(ret);
return -1;
}
sent += ret;
}
return sent;
}
/* clang-format off */
static 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 = offload_bind,
.connect = offload_connect,
.sendto = offload_sendto,
.recvfrom = offload_recvfrom,
.listen = NULL,
.accept = NULL,
.sendmsg = offload_sendmsg,
.getsockopt = NULL,
#if defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS)
.setsockopt = offload_setsockopt,
#else
.setsockopt = NULL,
#endif
};
/* clang-format on */
static int hl78xx_init_sockets(const struct device *dev)
{
int ret;
struct hl78xx_socket_data *socket_data = (struct hl78xx_socket_data *)dev->data;
socket_data->buf_pool = &mdm_recv_pool;
/* socket config */
ret = modem_socket_init(&socket_data->socket_config, &socket_data->sockets[0],
ARRAY_SIZE(socket_data->sockets), MDM_BASE_SOCKET_NUM, false,
&offload_socket_fd_op_vtable);
if (ret) {
goto error;
}
return 0;
error:
return ret;
}
static void socket_notify_data(int socket_id, int new_total, void *user_data)
{
int ret = 0;
struct modem_socket *sock;
struct hl78xx_data *data = (struct hl78xx_data *)user_data;
struct hl78xx_socket_data *socket_data =
(struct hl78xx_socket_data *)data->offload_dev->data;
if (!data || !socket_data) {
LOG_ERR("%s: invalid user_data", __func__);
return;
}
sock = modem_socket_from_id(&socket_data->socket_config, socket_id);
if (!sock) {
return;
}
/* Update the packet size */
ret = modem_socket_packet_size_update(&socket_data->socket_config, sock, new_total);
if (ret < 0) {
LOG_ERR("socket_id:%d left_bytes:%d err: %d", socket_id, new_total, ret);
}
if (new_total > 0) {
modem_socket_data_ready(&socket_data->socket_config, sock);
}
/* Duplicate/chat callback block removed; grouped versions live earlier */
}
#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) && defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS)
static int hl78xx_configure_chipper_suit(struct hl78xx_socket_data *socket_data)
{
const char *cmd_chipper_suit = "AT+KSSLCRYPTO=0,8,1,8192,4,4,3,0";
return modem_dynamic_cmd_send(
socket_data->mdata_global, NULL, cmd_chipper_suit, strlen(cmd_chipper_suit),
(const struct modem_chat_match *)hl78xx_get_ok_match(), 1, false);
}
/* send binary data via the K....STORE commands */
static ssize_t hl78xx_send_cert(struct hl78xx_socket_data *socket_data, const char *cert_data,
size_t cert_len, enum tls_credential_type cert_type)
{
int ret;
char send_buf[sizeof("AT+KPRIVKSTORE=#,####\r\n")];
int sock_written = 0;
if (!socket_data || !socket_data->mdata_global) {
return -EINVAL;
}
if (cert_len == 0 || !cert_data) {
LOG_ERR("Invalid certificate data or length");
return -EINVAL;
}
/** Certificate length exceeds maximum allowed size */
if (cert_len > MDM_MAX_CERT_LENGTH) {
return -EINVAL;
}
if (cert_type == TLS_CREDENTIAL_CA_CERTIFICATE ||
cert_type == TLS_CREDENTIAL_SERVER_CERTIFICATE) {
snprintk(send_buf, sizeof(send_buf), "AT+KCERTSTORE=%d,%d", (cert_type - 1),
cert_len);
} else if (cert_type == TLS_CREDENTIAL_PRIVATE_KEY) {
snprintk(send_buf, sizeof(send_buf), "AT+KPRIVKSTORE=0,%d", cert_len);
} else {
LOG_ERR("Unsupported certificate type: %d", cert_type);
return -EINVAL;
}
socket_data->socket_data_error = false;
if (k_mutex_lock(&socket_data->mdata_global->tx_lock, K_SECONDS(1)) < 0) {
errno = EBUSY;
return -1;
}
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, send_buf, strlen(send_buf),
(const struct modem_chat_match *)hl78xx_get_connect_matches(),
hl78xx_get_connect_matches_size(), false);
if (ret < 0) {
LOG_ERR("Error sending AT command %d", ret);
}
if (socket_data->socket_data_error) {
ret = -ENODEV;
errno = ENODEV;
goto cleanup;
}
modem_pipe_attach(socket_data->mdata_global->chat.pipe, modem_pipe_callback,
socket_data->mdata_global);
ret = send_data_buffer(socket_data, cert_data, cert_len, &sock_written);
if (ret < 0) {
goto cleanup;
}
ret = k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER);
if (ret < 0) {
goto cleanup;
}
ret = modem_pipe_transmit(socket_data->mdata_global->uart_pipe,
(uint8_t *)socket_data->mdata_global->buffers.eof_pattern,
socket_data->mdata_global->buffers.eof_pattern_size);
if (ret < 0) {
LOG_ERR("Error sending EOF pattern: %d", ret);
}
modem_chat_attach(&socket_data->mdata_global->chat, socket_data->mdata_global->uart_pipe);
ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, "", 0,
(const struct modem_chat_match *)hl78xx_get_ok_match(), 1,
false);
if (ret < 0) {
LOG_ERR("Final confirmation failed: %d", ret);
goto cleanup;
}
cleanup:
k_mutex_unlock(&socket_data->mdata_global->tx_lock);
return (ret < 0) ? -1 : sock_written;
}
static int map_credentials(struct hl78xx_socket_data *socket_data, const void *optval,
socklen_t optlen)
{
const sec_tag_t *sec_tags = (const sec_tag_t *)optval;
int ret = 0;
int tags_len;
sec_tag_t tag;
int i;
struct tls_credential *cert;
if ((optlen % sizeof(sec_tag_t)) != 0 || (optlen == 0)) {
return -EINVAL;
}
tags_len = optlen / sizeof(sec_tag_t);
/* For each tag, retrieve the credentials value and type: */
for (i = 0; i < tags_len; i++) {
tag = sec_tags[i];
cert = credential_next_get(tag, NULL);
while (cert != NULL) {
switch (cert->type) {
case TLS_CREDENTIAL_CA_CERTIFICATE:
LOG_DBG("TLS_CREDENTIAL_CA_CERTIFICATE tag: %d", tag);
break;
case TLS_CREDENTIAL_SERVER_CERTIFICATE:
LOG_DBG("TLS_CREDENTIAL_SERVER_CERTIFICATE tag: %d", tag);
break;
case TLS_CREDENTIAL_PRIVATE_KEY:
LOG_DBG("TLS_CREDENTIAL_PRIVATE_KEY tag: %d", tag);
break;
case TLS_CREDENTIAL_NONE:
case TLS_CREDENTIAL_PSK:
case TLS_CREDENTIAL_PSK_ID:
default:
/* Not handled */
return -EINVAL;
}
ret = hl78xx_send_cert(socket_data, cert->buf, cert->len, cert->type);
if (ret < 0) {
return ret;
}
cert = credential_next_get(tag, cert);
}
}
return 0;
}
#endif
static int hl78xx_socket_init(const struct device *dev)
{
struct hl78xx_socket_data *data = (struct hl78xx_socket_data *)dev->data;
data->offload_dev = dev;
/* Ensure the parent modem device pointer was set at static init time */
if (data->modem_dev == NULL) {
LOG_ERR("modem_dev not initialized for %s", dev->name);
return -EINVAL;
}
/* Ensure the modem device is ready before accessing its driver data */
if (!device_is_ready(data->modem_dev)) {
LOG_ERR("modem device %s not ready", data->modem_dev->name);
return -ENODEV;
}
if (data->modem_dev->data == NULL) {
LOG_ERR("modem device %s has no driver data yet", data->modem_dev->name);
return -EAGAIN;
}
data->mdata_global = (struct hl78xx_data *)data->modem_dev->data;
data->mdata_global->offload_dev = dev;
/* Keep original single global pointer usage but set via accessor. */
hl78xx_set_socket_global(data);
atomic_set(&data->mdata_global->state_leftover, 0);
return 0;
}
static void modem_net_iface_init(struct net_if *iface)
{
const struct device *dev = net_if_get_device(iface);
struct hl78xx_socket_data *data = dev->data;
/* startup trace */
if (!data->mdata_global) {
LOG_WRN("mdata_global not set for net iface init on %s", dev->name);
}
net_if_set_link_addr(
iface,
modem_get_mac(data->mac_addr,
data->mdata_global ? data->mdata_global->identity.imei : NULL),
sizeof(data->mac_addr), NET_LINK_ETHERNET);
data->net_iface = iface;
hl78xx_init_sockets(dev);
net_if_socket_offload_set(iface, offload_socket);
}
static struct offloaded_if_api api_funcs = {
.iface_api.init = modem_net_iface_init,
};
static bool offload_is_supported(int family, int type, int proto)
{
bool fam_ok = false;
#ifdef CONFIG_NET_IPV4
if (family == AF_INET) {
fam_ok = true;
}
#endif
#ifdef CONFIG_NET_IPV6
if (family == AF_INET6) {
fam_ok = true;
}
#endif
if (!fam_ok) {
return false;
}
if (!(type == SOCK_DGRAM || type == SOCK_STREAM)) {
return false;
}
if (proto == IPPROTO_TCP || proto == IPPROTO_UDP) {
return true;
}
#if defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS)
if (proto == IPPROTO_TLS_1_2) {
return true;
}
#endif
return false;
}
#define MODEM_HL78XX_DEFINE_OFFLOAD_INSTANCE(inst) \
static struct hl78xx_socket_data hl78xx_socket_data_##inst = { \
.modem_dev = DEVICE_DT_GET(DT_PARENT(DT_DRV_INST(inst))), \
}; \
NET_DEVICE_OFFLOAD_INIT( \
inst, "hl78xx_dev", hl78xx_socket_init, NULL, &hl78xx_socket_data_##inst, NULL, \
CONFIG_MODEM_HL78XX_OFFLOAD_INIT_PRIORITY, &api_funcs, MDM_MAX_DATA_LENGTH); \
\
NET_SOCKET_OFFLOAD_REGISTER(inst, CONFIG_NET_SOCKETS_OFFLOAD_PRIORITY, AF_UNSPEC, \
offload_is_supported, offload_socket);
#define MODEM_OFFLOAD_DEVICE_SWIR_HL78XX(inst) MODEM_HL78XX_DEFINE_OFFLOAD_INSTANCE(inst)
#define DT_DRV_COMPAT swir_hl7812_offload
DT_INST_FOREACH_STATUS_OKAY(MODEM_OFFLOAD_DEVICE_SWIR_HL78XX)
#undef DT_DRV_COMPAT
#define DT_DRV_COMPAT swir_hl7800_offload
DT_INST_FOREACH_STATUS_OKAY(MODEM_OFFLOAD_DEVICE_SWIR_HL78XX)
#undef DT_DRV_COMPAT