samples/net: Add cellular modem sample

This commit adds a sample application which demonstrates how
to use the new driver and modules. The sample uses power
management to turn on the modem, uses network management
to wait for L4 connected, then uses DNS to get the IP
of the server running the python script found in the
server folder, which echoes back data recevied to it.
A packet containing psudo random data is then sent to
the server, which the echoes it back. To validate the
capability of the driver to restart the modem, the
modem is restarted, and the packet is sent again.

The server is hosted by linode, and uses the domain
name test-endpoint.com

Signed-off-by: Bjarki Arge Andreasen <baa@trackunit.com>
This commit is contained in:
Bjarki Arge Andreasen 2023-06-07 12:40:29 +02:00 committed by Carles Cufí
commit c0c9d6f7d6
12 changed files with 693 additions and 0 deletions

View file

@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(cellular_modem)
target_sources(app PRIVATE src/main.c)

View file

@ -0,0 +1,57 @@
.. _cellular_modem_sample:
Cellular Modem Sample
########################
Overview
********
This sample consists of a simple application which powers on
the modem, brings up the net interface, then sends a packet
with pseudo random data to the endpoint test-endpoint.com,
which is a publicly hosted server which runs the Python
script found in the server folder. DNS is used to look
up the IP of test-endpoint.com.
Notes
*****
This sample uses the devicetree alias modem to identify
the modem instance to use. The sample also presumes that
the modem driver creates the only network interface.
Setup
*****
Start by setting up the devicetree with the required
devicetree node:
.. code-block:: devicetree
/dts-v1/;
/ {
aliases {
modem = &modem;
};
};
&usart2 {
pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3 &usart2_rts_pa1 &usart2_cts_pa0>;
pinctrl-names = "default";
current-speed = <115200>;
hw-flow-control;
status = "okay";
modem: modem {
compatible = "quectel,bg9x";
mdm-power-gpios = <&gpioe 2 GPIO_ACTIVE_HIGH>;
mdm-reset-gpios = <&gpioe 3 GPIO_ACTIVE_HIGH>;
status = "okay";
};
};
Next, the UART API must be specified using ``CONFIG_UART_INTERRUPT_DRIVEN=y`` or
``CONFIG_UART_ASYNC_API=y``. The driver doesn't support UART polling.
Lastly, the APN must be configured using ``CONFIG_MODEM_CELLULAR_APN=""``.

View file

@ -0,0 +1,5 @@
# Copyright (c) 2023, Bjarki Arge Andreasen
# SPDX-License-Identifier: Apache-2.0
CONFIG_UART_ASYNC_API=y
CONFIG_MODEM_CELLULAR_APN="internet"

View file

@ -0,0 +1,38 @@
/ {
aliases {
modem-uart = &usart2;
modem = &modem;
};
};
&gpioh {
misc_fixed_usart2 {
gpio-hog;
gpios = <13 GPIO_ACTIVE_HIGH>;
output-high;
};
};
&gpdma1 {
status = "okay";
};
/* BG95 */
&usart2 {
pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3 &usart2_rts_pa1 &usart2_cts_pa0>;
pinctrl-names = "default";
current-speed = <115200>;
hw-flow-control;
dmas = <&gpdma1 0 27 STM32_DMA_PERIPH_TX
&gpdma1 1 26 STM32_DMA_PERIPH_RX>;
dma-names = "tx", "rx";
status = "okay";
modem: modem {
compatible = "quectel,bg95";
mdm-power-gpios = <&gpioe 2 GPIO_ACTIVE_HIGH>;
status = "okay";
};
};

View file

@ -0,0 +1,24 @@
# Copyright (c) 2023 Bjarki Arge Andreasen
# SPDX-License-Identifier: Apache-2.0
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_NATIVE=y
CONFIG_NET_L2_PPP=y
CONFIG_NET_IPV4=y
CONFIG_NET_UDP=y
CONFIG_NET_SOCKETS=y
# DNS
CONFIG_DNS_RESOLVER=y
CONFIG_NET_L2_PPP_OPTION_DNS_USE=y
# Network management
CONFIG_NET_MGMT=y
CONFIG_NET_MGMT_EVENT=y
CONFIG_NET_CONNECTION_MANAGER=y
# Modem driver
CONFIG_MODEM=y
CONFIG_PM_DEVICE=y
CONFIG_MODEM_CELLULAR=y

View file

@ -0,0 +1,13 @@
sample:
description: Sample for cellular modem
name: Sample for cellular modem using native networking
common:
tags: cellular modem
tests:
sample.net.cellular_modem:
tags: cellular modem
filter: dt_alias_exists("modem")
platform_allow:
- b_u585i_iot02a
integration_platforms:
- b_u585i_iot02a

View file

@ -0,0 +1,22 @@
# Copyright (c) 2023, Bjarki Arge Andreasen
# SPDX-License-Identifier: Apache-2.0
import signal
from te_udp_echo import TEUDPEcho
from te_udp_receive import TEUDPReceive
udp_echo = TEUDPEcho()
udp_receive = TEUDPReceive()
udp_echo.start()
udp_receive.start()
print("started")
def terminate_handler(a, b):
udp_echo.stop()
udp_receive.stop()
print("stopped")
signal.signal(signal.SIGTERM, terminate_handler)
signal.signal(signal.SIGINT, terminate_handler)

View file

@ -0,0 +1,40 @@
# Copyright (c) 2023, Bjarki Arge Andreasen
# SPDX-License-Identifier: Apache-2.0
import socket
import threading
import select
class TEUDPEcho():
def __init__(self):
self.running = True
self.thread = threading.Thread(target=self._target_)
def start(self):
self.thread.start()
def stop(self):
self.running = False
self.thread.join(1)
def _target_(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(False)
sock.bind(('0.0.0.0', 7780))
while self.running:
try:
ready_to_read, _, _ = select.select([sock], [sock], [], 0.5)
if not ready_to_read:
continue
data, address = sock.recvfrom(4096)
print(f'udp echo {len(data)} bytes to {address[0]}:{address[1]}')
sock.sendto(data, address)
except Exception as e:
print(e)
break
sock.close()

View file

@ -0,0 +1,107 @@
# Copyright (c) 2023, Bjarki Arge Andreasen
# SPDX-License-Identifier: Apache-2.0
import socket
import threading
import select
import time
import copy
class TEUDPReceiveSession():
def __init__(self, address, timeout = 1):
self.address = address
self.last_packet_received_at = time.monotonic()
self.timeout = timeout
self.packets_received = 0
self.packets_dropped = 0
def get_address(self):
return self.address
def on_packet_received(self, data):
if self._validate_packet_(data):
self.packets_received += 1
else:
self.packets_dropped += 1
self.last_packet_received_at = time.monotonic()
def update(self):
if (time.monotonic() - self.last_packet_received_at) > self.timeout:
return (self.packets_received, self.packets_dropped)
return None
def _validate_packet_(self, data: bytes) -> bool:
prng_state = 1234
for b in data:
prng_state = ((1103515245 * prng_state) + 12345) % (1 << 31)
if prng_state & 0xFF != b:
return False
return True
class TEUDPReceive():
def __init__(self):
self.running = True
self.thread = threading.Thread(target=self._target_)
self.sessions = []
def start(self):
self.thread.start()
def stop(self):
self.running = False
self.thread.join(1)
def _target_(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(False)
sock.bind(('0.0.0.0', 7781))
while self.running:
try:
ready_to_read, _, _ = select.select([sock], [sock], [], 0.5)
if not ready_to_read:
self._update_sessions_(sock)
continue
data, address = sock.recvfrom(4096)
print(f'udp received {len(data)} bytes -> {address[0]}:{address[1]}')
session = self._get_session_by_address_(address)
session.on_packet_received(data)
except Exception as e:
print(e)
break
sock.close()
def _get_session_by_address_(self, address) -> TEUDPReceiveSession:
# Search for existing session
for session in self.sessions:
if session.get_address() == address:
return session
# Create and return new session
print(f'Created session for {address[0]}:{address[1]}')
self.sessions.append(TEUDPReceiveSession(address, 2))
return self.sessions[-1]
def _update_sessions_(self, sock):
sessions = copy.copy(self.sessions)
for session in sessions:
result = session.update()
if result is None:
continue
response = bytes([result[0], result[1]])
print(f'Sending result {response} to address {session.get_address()}')
sock.sendto(response, session.get_address())
print(f'Removing session for address {session.get_address()}')
self.sessions.remove(session)

View file

@ -0,0 +1,330 @@
/*
* 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>
#define SAMPLE_TEST_ENDPOINT_HOSTNAME ("test-endpoint.com")
#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)
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;
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 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)
{
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 = zsock_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (socket_fd < 0) {
printk("Failed to open socket\n");
return -1;
}
printk("Socket opened\n");
if (ai_addr->sa_family == AF_INET) {
net_sin(ai_addr)->sin_port = htons(SAMPLE_TEST_ENDPOINT_UDP_ECHO_PORT);
} else if (ai_addr->sa_family == AF_INET6) {
net_sin6(ai_addr)->sin6_port = htons(SAMPLE_TEST_ENDPOINT_UDP_ECHO_PORT);
} else {
printk("Unsupported address family\n");
return -1;
}
for (uint32_t i = 0; i < SAMPLE_TEST_ECHO_PACKETS; i++) {
send_start_ms = k_uptime_get_32();
ret = zsock_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;
}
ret = zsock_recv(socket_fd, sample_recv_buffer, sizeof(sample_recv_buffer), 0);
if (ret != sizeof(sample_test_packet)) {
printk("Echoed sample test packet has incorrect size\n");
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 %u packets of %u packets\n", packets_sent,
SAMPLE_TEST_ECHO_PACKETS);
printk("Average time per echo: %u ms\n",
accumulated_ms / packets_sent);
printk("Close UDP socket\n");
ret = zsock_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)
{
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 = zsock_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (socket_fd < 0) {
printk("Failed to open socket\n");
return -1;
}
printk("Socket opened\n");
if (ai_addr->sa_family == AF_INET) {
net_sin(ai_addr)->sin_port = htons(SAMPLE_TEST_ENDPOINT_UDP_RECEIVE_PORT);
} else if (ai_addr->sa_family == AF_INET6) {
net_sin6(ai_addr)->sin6_port = htons(SAMPLE_TEST_ENDPOINT_UDP_RECEIVE_PORT);
} else {
printk("Unsupported address family\n");
return -1;
}
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 = zsock_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 = zsock_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 packets\n", packets_received);
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 = zsock_close(socket_fd);
if (ret < 0) {
printk("Failed to close socket\n");
return -1;
}
return 0;
}
int main(void)
{
uint32_t raised_event;
const void *info;
size_t info_len;
int ret;
init_sample_test_packet();
printk("Powering on modem\n");
pm_device_action_run(modem, PM_DEVICE_ACTION_RESUME);
printk("Bring up network interface\n");
ret = net_if_up(net_if_get_default());
if (ret < 0) {
printk("Failed to bring up network interface\n");
return -1;
}
printk("Waiting for L4 connected\n");
ret = net_mgmt_event_wait_on_iface(net_if_get_default(),
NET_EVENT_L4_CONNECTED, &raised_event, &info,
&info_len, K_SECONDS(120));
if (ret != 0) {
printk("L4 was not connected in time\n");
return -1;
}
printk("Waiting for DNS server added\n");
ret = net_mgmt_event_wait_on_iface(net_if_get_default(),
NET_EVENT_DNS_SERVER_ADD, &raised_event, &info,
&info_len, K_SECONDS(10));
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;
}
ret = sample_echo_packet(&sample_test_dns_addrinfo.ai_addr,
sample_test_dns_addrinfo.ai_addrlen);
if (ret < 0) {
printk("Failed to send echo\n");
return -1;
}
ret = sample_transmit_packets(&sample_test_dns_addrinfo.ai_addr,
sample_test_dns_addrinfo.ai_addrlen);
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);
ret = net_mgmt_event_wait_on_iface(net_if_get_default(),
NET_EVENT_L4_CONNECTED, &raised_event, &info,
&info_len, K_SECONDS(60));
if (ret != 0) {
printk("L4 was not connected in time\n");
return -1;
}
ret = sample_echo_packet(&sample_test_dns_addrinfo.ai_addr,
sample_test_dns_addrinfo.ai_addrlen);
if (ret < 0) {
printk("Failed to send echo after restart\n");
return -1;
}
ret = net_if_down(net_if_get_default());
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;
}

View file

@ -0,0 +1,12 @@
CONFIG_TEST_RANDOM_GENERATOR=y
CONFIG_GSM_MUX=y
CONFIG_UART_MUX=y
CONFIG_MODEM_GSM_APN="internet"
CONFIG_MAIN_STACK_SIZE=8192
CONFIG_MODEM_GSM_QUECTEL=y
CONFIG_MODEM_SHELL=n
CONFIG_NET_MGMT_EVENT_INFO=y
CONFIG_NET_MGMT_EVENT_MONITOR=y
CONFIG_NET_MGMT_EVENT_MONITOR_AUTO_START=y
CONFIG_NET_LOG=y
CONFIG_NET_MGMT_EVENT_LOG_LEVEL_DBG=y

View file

@ -0,0 +1,37 @@
/ {
aliases {
modem-uart = &usart2;
modem = &modem;
};
};
&gpioh {
misc_fixed_usart2 {
gpio-hog;
gpios = <13 GPIO_ACTIVE_HIGH>;
output-high;
};
};
&gpdma1 {
status = "okay";
};
/* BG95 */
&usart2 {
pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3 &usart2_rts_pa1 &usart2_cts_pa0>;
pinctrl-names = "default";
current-speed = <115200>;
hw-flow-control;
dmas = <&gpdma1 0 27 STM32_DMA_PERIPH_TX
&gpdma1 1 26 STM32_DMA_PERIPH_RX>;
dma-names = "tx", "rx";
status = "okay";
modem: modem {
compatible = "zephyr,gsm-ppp";
status = "okay";
};
};