blob: b6afbf6a97f5b54b139aa0e64dc84975b211ab91 [file] [log] [blame]
/*
* Copyright (c) 2023, Bjarki Arge Andreasen
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/net/socket.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/dns_resolve.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <string.h>
#include <zephyr/drivers/cellular.h>
#define L4_EVENT_MASK \
(NET_EVENT_L4_CONNECTED | NET_EVENT_L4_DISCONNECTED | NET_EVENT_DNS_SERVER_ADD)
#define SAMPLE_TEST_ENDPOINT_HOSTNAME CONFIG_SAMPLE_CELLULAR_MODEM_ENDPOINT_HOSTNAME
#define SAMPLE_TEST_ENDPOINT_UDP_ECHO_PORT (7780)
#define SAMPLE_TEST_ENDPOINT_UDP_RECEIVE_PORT (7781)
#define SAMPLE_TEST_PACKET_SIZE (1024)
#define SAMPLE_TEST_ECHO_PACKETS (16)
#define SAMPLE_TEST_TRANSMIT_PACKETS (128)
#define L4_CONNECTED 1
#define L4_DNS_ADDED 2
const struct device *modem = DEVICE_DT_GET(DT_ALIAS(modem));
static uint8_t sample_test_packet[SAMPLE_TEST_PACKET_SIZE];
static uint8_t sample_recv_buffer[SAMPLE_TEST_PACKET_SIZE];
static bool sample_test_dns_in_progress;
static struct dns_addrinfo sample_test_dns_addrinfo;
struct net_if *ppp_iface;
K_EVENT_DEFINE(l4_event);
K_SEM_DEFINE(dns_query_sem, 0, 1);
static uint8_t sample_prng_random(void)
{
static uint32_t prng_state = 1234;
prng_state = ((1103515245 * prng_state) + 12345) % (1U << 31);
return (uint8_t)(prng_state & 0xFF);
}
static void init_sample_test_packet(void)
{
for (size_t i = 0; i < sizeof(sample_test_packet); i++) {
sample_test_packet[i] = sample_prng_random();
}
}
static void print_cellular_info(void)
{
int rc;
int16_t rssi;
char buffer[64];
rc = cellular_get_signal(modem, CELLULAR_SIGNAL_RSSI, &rssi);
if (!rc) {
printk("RSSI %d\n", rssi);
}
rc = cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_IMEI, &buffer[0], sizeof(buffer));
if (!rc) {
printk("IMEI: %s\n", buffer);
}
rc = cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_MODEL_ID, &buffer[0],
sizeof(buffer));
if (!rc) {
printk("MODEL_ID: %s\n", buffer);
}
rc = cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_MANUFACTURER, &buffer[0],
sizeof(buffer));
if (!rc) {
printk("MANUFACTURER: %s\n", buffer);
}
rc = cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_SIM_IMSI, &buffer[0],
sizeof(buffer));
if (!rc) {
printk("SIM_IMSI: %s\n", buffer);
}
rc = cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_SIM_ICCID, &buffer[0],
sizeof(buffer));
if (!rc) {
printk("SIM_ICCID: %s\n", buffer);
}
rc = cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_FW_VERSION, &buffer[0],
sizeof(buffer));
if (!rc) {
printk("FW_VERSION: %s\n", buffer);
}
}
#ifdef CONFIG_SAMPLE_CELLULAR_MODEM_AUTO_APN
struct apn_profile {
const char *apn;
const char *imsi_list;
};
/* Build the static table */
static const struct apn_profile apn_profiles[] = {
{ CONFIG_SAMPLE_CELLULAR_APN_0, CONFIG_SAMPLE_CELLULAR_IMSI_LIST_0 },
{ CONFIG_SAMPLE_CELLULAR_APN_1, CONFIG_SAMPLE_CELLULAR_IMSI_LIST_1 },
{ CONFIG_SAMPLE_CELLULAR_APN_2, CONFIG_SAMPLE_CELLULAR_IMSI_LIST_2 },
{ CONFIG_SAMPLE_CELLULAR_APN_3, CONFIG_SAMPLE_CELLULAR_IMSI_LIST_3 },
};
/* Helper function to skip whitespace */
static const char *skip_whitespace(const char *ptr)
{
while (*ptr == ' ' || *ptr == '\t') {
++ptr;
}
return ptr;
}
/* Helper function to find the end of current profile entry */
static bool list_matches_imsi(const char *list, const char *imsi)
{
for (const char *p = list; *p; ) {
p = skip_whitespace(p);
if (!*p) {
break;
}
/* copy one token from the list */
char tok[7];
size_t len = 0;
while (*p && *p != ' ' && *p != '\t' && *p != ',' && len < sizeof(tok) - 1) {
tok[len++] = *p++;
}
tok[len] = '\0';
if (len >= 5 && len <= 6 && !strncmp(imsi, tok, len)) {
return true; /* prefix matches */
}
}
return false;
}
static int modem_cellular_find_apn(char *dst, size_t dst_sz, const char *key)
{
for (size_t i = 0; i < ARRAY_SIZE(apn_profiles); i++) {
const struct apn_profile *p = &apn_profiles[i];
if (p->apn[0] == '\0') {
continue;
}
if (p->apn[0] && list_matches_imsi(p->imsi_list, key)) {
strncpy(dst, p->apn, dst_sz - 1);
dst[dst_sz - 1] = '\0';
return 0;
}
}
return -ENOENT;
}
static void modem_event_cb(const struct device *dev, enum cellular_event evt, const void *payload,
void *user_data)
{
ARG_UNUSED(user_data);
if (evt != CELLULAR_EVENT_MODEM_INFO_CHANGED) {
return;
}
const struct cellular_evt_modem_info *mi = payload;
if (!mi || mi->field != CELLULAR_MODEM_INFO_SIM_IMSI) {
return; /* not the IMSI notification */
}
char imsi[32] = {0};
if (cellular_get_modem_info(dev, CELLULAR_MODEM_INFO_SIM_IMSI, imsi, sizeof(imsi)) != 0) {
return;
}
/* Buffer for the APN we may discover */
char apn[32] = {0};
/* Try MCC+MNC with 6 digits first, then 5 digits */
for (size_t len = 6; len >= 5; len--) {
if (strlen(imsi) < len) {
continue;
}
char key[7] = {0};
memcpy(key, imsi, len);
if (modem_cellular_find_apn(apn, sizeof(apn), key) == 0) {
int rc = cellular_set_apn(dev, apn);
switch (rc) {
case 0:
printk("Auto-selected APN: %s\n", apn);
break;
case -EALREADY:
printk("APN %s already active\n", apn);
break;
case -EBUSY:
printk("Driver busy, cannot change APN now\n");
break;
default:
printk("Driver rejected APN %s (err %d)\n", apn, rc);
break;
}
return;
}
}
printk("No APN profile matches IMSI %s - waiting for manual APN\n", imsi);
}
#endif
static void sample_dns_request_result(enum dns_resolve_status status, struct dns_addrinfo *info,
void *user_data)
{
if (sample_test_dns_in_progress == false) {
return;
}
if (status != DNS_EAI_INPROGRESS) {
return;
}
sample_test_dns_in_progress = false;
sample_test_dns_addrinfo = *info;
k_sem_give(&dns_query_sem);
}
static int sample_dns_request(void)
{
static uint16_t dns_id;
int ret;
sample_test_dns_in_progress = true;
ret = dns_get_addr_info(SAMPLE_TEST_ENDPOINT_HOSTNAME,
DNS_QUERY_TYPE_A,
&dns_id,
sample_dns_request_result,
NULL,
19000);
if (ret < 0) {
return -EAGAIN;
}
if (k_sem_take(&dns_query_sem, K_SECONDS(20)) < 0) {
return -EAGAIN;
}
return 0;
}
int sample_echo_packet(struct sockaddr *ai_addr, socklen_t ai_addrlen, uint16_t *port)
{
int ret;
int socket_fd;
uint32_t packets_sent = 0;
uint32_t send_start_ms;
uint32_t echo_received_ms;
uint32_t accumulated_ms = 0;
printk("Opening UDP socket\n");
socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (socket_fd < 0) {
printk("Failed to open socket (%d)\n", errno);
return -1;
}
{
const struct timeval tv = { .tv_sec = 10 };
if (zsock_setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
printk("Failed to set socket receive timeout (%d)\n", errno);
return -1;
}
}
printk("Socket opened\n");
*port = htons(SAMPLE_TEST_ENDPOINT_UDP_ECHO_PORT);
for (uint32_t i = 0; i < SAMPLE_TEST_ECHO_PACKETS; i++) {
printk("Sending echo packet\n");
send_start_ms = k_uptime_get_32();
ret = sendto(socket_fd, sample_test_packet, sizeof(sample_test_packet), 0,
ai_addr, ai_addrlen);
if (ret < sizeof(sample_test_packet)) {
printk("Failed to send sample test packet\n");
continue;
}
printk("Receiving echoed packet\n");
ret = recv(socket_fd, sample_recv_buffer, sizeof(sample_recv_buffer), 0);
if (ret != sizeof(sample_test_packet)) {
if (ret == -1) {
printk("Failed to receive echoed sample test packet (%d)\n", errno);
} else {
printk("Echoed sample test packet has incorrect size (%d)\n", ret);
}
continue;
}
echo_received_ms = k_uptime_get_32();
if (memcmp(sample_test_packet, sample_recv_buffer,
sizeof(sample_recv_buffer)) != 0) {
printk("Echoed sample test packet data mismatch\n");
continue;
}
packets_sent++;
accumulated_ms += echo_received_ms - send_start_ms;
printk("Echo transmit time %ums\n", echo_received_ms - send_start_ms);
}
printk("Successfully sent and received %u of %u packets\n", packets_sent,
SAMPLE_TEST_ECHO_PACKETS);
if (packets_sent > 0) {
printk("Average time per successful echo: %u ms\n",
accumulated_ms / packets_sent);
}
printk("Close UDP socket\n");
ret = close(socket_fd);
if (ret < 0) {
printk("Failed to close socket\n");
return -1;
}
return 0;
}
int sample_transmit_packets(struct sockaddr *ai_addr, socklen_t ai_addrlen, uint16_t *port)
{
int ret;
int socket_fd;
uint32_t packets_sent = 0;
uint32_t packets_received;
uint32_t packets_dropped;
uint32_t send_start_ms;
uint32_t send_end_ms;
printk("Opening UDP socket\n");
socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (socket_fd < 0) {
printk("Failed to open socket\n");
return -1;
}
printk("Socket opened\n");
*port = htons(SAMPLE_TEST_ENDPOINT_UDP_RECEIVE_PORT);
printk("Sending %u packets\n", SAMPLE_TEST_TRANSMIT_PACKETS);
send_start_ms = k_uptime_get_32();
for (uint32_t i = 0; i < SAMPLE_TEST_TRANSMIT_PACKETS; i++) {
ret = sendto(socket_fd, sample_test_packet, sizeof(sample_test_packet), 0,
ai_addr, ai_addrlen);
if (ret < sizeof(sample_test_packet)) {
printk("Failed to send sample test packet\n");
break;
}
packets_sent++;
}
send_end_ms = k_uptime_get_32();
printk("Awaiting response from server\n");
ret = recv(socket_fd, sample_recv_buffer, sizeof(sample_recv_buffer), 0);
if (ret != 2) {
printk("Invalid response\n");
return -1;
}
packets_received = sample_recv_buffer[0];
packets_dropped = sample_recv_buffer[1];
printk("Server received %u/%u packets\n", packets_received, packets_sent);
printk("Server dropped %u packets\n", packets_dropped);
printk("Time elapsed sending packets %ums\n", send_end_ms - send_start_ms);
printk("Throughput %u bytes/s\n",
((SAMPLE_TEST_PACKET_SIZE * SAMPLE_TEST_TRANSMIT_PACKETS) * 1000) /
(send_end_ms - send_start_ms));
printk("Close UDP socket\n");
ret = close(socket_fd);
if (ret < 0) {
printk("Failed to close socket\n");
return -1;
}
return 0;
}
static void l4_event_handler(uint64_t event, struct net_if *iface, void *info, size_t info_length,
void *user_data)
{
if (iface != ppp_iface) {
return;
}
switch (event) {
case NET_EVENT_L4_CONNECTED:
k_event_post(&l4_event, L4_CONNECTED);
break;
case NET_EVENT_DNS_SERVER_ADD:
k_event_post(&l4_event, L4_DNS_ADDED);
break;
case NET_EVENT_L4_DISCONNECTED:
k_event_set(&l4_event, 0);
break;
default:
break;
}
}
NET_MGMT_REGISTER_EVENT_HANDLER(l4_events, L4_EVENT_MASK, l4_event_handler, NULL);
int main(void)
{
uint16_t *port;
int ret;
#ifdef CONFIG_SAMPLE_CELLULAR_MODEM_AUTO_APN
/* subscribe before powering the modem so we catch the IMSI event */
cellular_set_callback(modem, CELLULAR_EVENT_MODEM_INFO_CHANGED, modem_event_cb, NULL);
#endif
init_sample_test_packet();
ppp_iface = net_if_get_first_by_type(&NET_L2_GET_NAME(PPP));
printk("Powering on modem\n");
pm_device_action_run(modem, PM_DEVICE_ACTION_RESUME);
printk("Bring up network interface\n");
ret = net_if_up(ppp_iface);
if (ret < 0) {
printk("Failed to bring up network interface\n");
return -1;
}
printk("Waiting for L4 connected\n");
ret = k_event_wait(&l4_event, L4_CONNECTED, false, K_SECONDS(120));
if (ret != L4_CONNECTED) {
printk("L4 was not connected in time\n");
return -1;
}
printk("Waiting for DNS server added\n");
ret = k_event_wait(&l4_event, L4_DNS_ADDED, false, K_SECONDS(10));
if (ret != L4_DNS_ADDED) {
printk("DNS server was not added in time\n");
return -1;
}
printk("Retrieving cellular info\n");
print_cellular_info();
printk("Performing DNS lookup of %s\n", SAMPLE_TEST_ENDPOINT_HOSTNAME);
ret = sample_dns_request();
if (ret < 0) {
printk("DNS query failed\n");
return -1;
}
{
char ip_str[INET6_ADDRSTRLEN];
const void *src;
switch (sample_test_dns_addrinfo.ai_addr.sa_family) {
case AF_INET:
src = &net_sin(&sample_test_dns_addrinfo.ai_addr)->sin_addr;
port = &net_sin(&sample_test_dns_addrinfo.ai_addr)->sin_port;
break;
case AF_INET6:
src = &net_sin6(&sample_test_dns_addrinfo.ai_addr)->sin6_addr;
port = &net_sin6(&sample_test_dns_addrinfo.ai_addr)->sin6_port;
break;
default:
printk("Unsupported address family\n");
return -1;
}
inet_ntop(sample_test_dns_addrinfo.ai_addr.sa_family, src, ip_str, sizeof(ip_str));
printk("Resolved to %s\n", ip_str);
}
ret = sample_echo_packet(&sample_test_dns_addrinfo.ai_addr,
sample_test_dns_addrinfo.ai_addrlen, port);
if (ret < 0) {
printk("Failed to send echos\n");
return -1;
}
ret = sample_transmit_packets(&sample_test_dns_addrinfo.ai_addr,
sample_test_dns_addrinfo.ai_addrlen, port);
if (ret < 0) {
printk("Failed to send packets\n");
return -1;
}
printk("Restart modem\n");
ret = pm_device_action_run(modem, PM_DEVICE_ACTION_SUSPEND);
if (ret != 0) {
printk("Failed to power down modem\n");
return -1;
}
pm_device_action_run(modem, PM_DEVICE_ACTION_RESUME);
printk("Waiting for L4 connected\n");
ret = k_event_wait(&l4_event, L4_CONNECTED, false, K_SECONDS(120));
if (ret != L4_CONNECTED) {
printk("L4 was not connected in time\n");
return -1;
}
printk("L4 connected\n");
/* Wait a bit to avoid (unsuccessfully) trying to send the first echo packet too quickly. */
k_sleep(K_SECONDS(5));
ret = sample_echo_packet(&sample_test_dns_addrinfo.ai_addr,
sample_test_dns_addrinfo.ai_addrlen, port);
if (ret < 0) {
printk("Failed to send echos after restart\n");
return -1;
}
ret = net_if_down(ppp_iface);
if (ret < 0) {
printk("Failed to bring down network interface\n");
return -1;
}
printk("Powering down modem\n");
ret = pm_device_action_run(modem, PM_DEVICE_ACTION_SUSPEND);
if (ret != 0) {
printk("Failed to power down modem\n");
return -1;
}
printk("Sample complete\n");
return 0;
}