diff --git a/samples/net/cellular_modem/CMakeLists.txt b/samples/net/cellular_modem/CMakeLists.txt new file mode 100644 index 00000000000..c9a32ece4c1 --- /dev/null +++ b/samples/net/cellular_modem/CMakeLists.txt @@ -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) diff --git a/samples/net/cellular_modem/README.rst b/samples/net/cellular_modem/README.rst new file mode 100644 index 00000000000..7c30a850705 --- /dev/null +++ b/samples/net/cellular_modem/README.rst @@ -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=""``. diff --git a/samples/net/cellular_modem/boards/b_u585i_iot02a.conf b/samples/net/cellular_modem/boards/b_u585i_iot02a.conf new file mode 100644 index 00000000000..f354fe4062f --- /dev/null +++ b/samples/net/cellular_modem/boards/b_u585i_iot02a.conf @@ -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" diff --git a/samples/net/cellular_modem/boards/b_u585i_iot02a.overlay b/samples/net/cellular_modem/boards/b_u585i_iot02a.overlay new file mode 100644 index 00000000000..69e23bf4ead --- /dev/null +++ b/samples/net/cellular_modem/boards/b_u585i_iot02a.overlay @@ -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"; + }; +}; diff --git a/samples/net/cellular_modem/prj.conf b/samples/net/cellular_modem/prj.conf new file mode 100644 index 00000000000..52d0ca26bde --- /dev/null +++ b/samples/net/cellular_modem/prj.conf @@ -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 diff --git a/samples/net/cellular_modem/sample.yaml b/samples/net/cellular_modem/sample.yaml new file mode 100644 index 00000000000..14c275d3919 --- /dev/null +++ b/samples/net/cellular_modem/sample.yaml @@ -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 diff --git a/samples/net/cellular_modem/server/te.py b/samples/net/cellular_modem/server/te.py new file mode 100755 index 00000000000..d2339b20964 --- /dev/null +++ b/samples/net/cellular_modem/server/te.py @@ -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) diff --git a/samples/net/cellular_modem/server/te_udp_echo.py b/samples/net/cellular_modem/server/te_udp_echo.py new file mode 100755 index 00000000000..dbf9cd3aa98 --- /dev/null +++ b/samples/net/cellular_modem/server/te_udp_echo.py @@ -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() diff --git a/samples/net/cellular_modem/server/te_udp_receive.py b/samples/net/cellular_modem/server/te_udp_receive.py new file mode 100755 index 00000000000..52ae2b47b47 --- /dev/null +++ b/samples/net/cellular_modem/server/te_udp_receive.py @@ -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) diff --git a/samples/net/cellular_modem/src/main.c b/samples/net/cellular_modem/src/main.c new file mode 100644 index 00000000000..95e1c45f160 --- /dev/null +++ b/samples/net/cellular_modem/src/main.c @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2023, Bjarki Arge Andreasen + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/samples/net/gsm_modem/boards/b_u585i_iot02a.conf b/samples/net/gsm_modem/boards/b_u585i_iot02a.conf new file mode 100644 index 00000000000..c344244d206 --- /dev/null +++ b/samples/net/gsm_modem/boards/b_u585i_iot02a.conf @@ -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 diff --git a/samples/net/gsm_modem/boards/b_u585i_iot02a.overlay b/samples/net/gsm_modem/boards/b_u585i_iot02a.overlay new file mode 100644 index 00000000000..f8d862f4737 --- /dev/null +++ b/samples/net/gsm_modem/boards/b_u585i_iot02a.overlay @@ -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"; + }; +};