diff --git a/drivers/modem/CMakeLists.txt b/drivers/modem/CMakeLists.txt index adc6614dc7fe5..5553cb2e5467d 100644 --- a/drivers/modem/CMakeLists.txt +++ b/drivers/modem/CMakeLists.txt @@ -31,9 +31,7 @@ if (CONFIG_MODEM_HL7800) zephyr_library_sources(hl7800.c) endif() -if (CONFIG_MODEM_SIM7080) - zephyr_library_sources(simcom-sim7080.c) -endif() +add_subdirectory(simcom) zephyr_library_sources_ifdef(CONFIG_MODEM_CELLULAR modem_cellular.c) zephyr_library_sources_ifdef(CONFIG_MODEM_AT_USER_PIPE modem_at_user_pipe.c) diff --git a/drivers/modem/Kconfig b/drivers/modem/Kconfig index b8e66ef427482..02de18fee7a97 100644 --- a/drivers/modem/Kconfig +++ b/drivers/modem/Kconfig @@ -135,6 +135,7 @@ config MODEM_CMD_HANDLER config MODEM_CMD_HANDLER_MAX_PARAM_COUNT int "Maximum number of params parsed per command" depends on MODEM_CMD_HANDLER + default 14 if MODEM_SIM7080 default 6 help This option sets the maximum number of parameters which may be @@ -194,6 +195,6 @@ source "drivers/modem/Kconfig.cellular" source "drivers/modem/Kconfig.at_shell" source "drivers/modem/Kconfig.hl7800" -source "drivers/modem/Kconfig.simcom-sim7080" +source "drivers/modem/simcom/Kconfig" endif # MODEM diff --git a/drivers/modem/simcom/CMakeLists.txt b/drivers/modem/simcom/CMakeLists.txt new file mode 100644 index 0000000000000..49028eb9036d6 --- /dev/null +++ b/drivers/modem/simcom/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Metratec GmbH. +# SPDX-License-Identifier: Apache-2.0 + +# zephyr-keep-sorted-start +add_subdirectory_ifdef(CONFIG_MODEM_SIM7080 sim7080) +# zephyr-keep-sorted-stop diff --git a/drivers/modem/simcom/Kconfig b/drivers/modem/simcom/Kconfig new file mode 100644 index 0000000000000..c8f37c1a55207 --- /dev/null +++ b/drivers/modem/simcom/Kconfig @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Metratec GmbH. +# SPDX-License-Identifier: Apache-2.0 + +# zephyr-keep-sorted-start +source "drivers/modem/simcom/sim7080/Kconfig" +# zephyr-keep-sorted-stop diff --git a/drivers/modem/simcom/sim7080/CMakeLists.txt b/drivers/modem/simcom/sim7080/CMakeLists.txt new file mode 100644 index 0000000000000..1eba58cb002e6 --- /dev/null +++ b/drivers/modem/simcom/sim7080/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright (c) 2025 Metratec GmbH. +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_include_directories(${ZEPHYR_BASE}/drivers/modem) +zephyr_library_include_directories(${ZEPHYR_BASE}/subsys/net/ip) +zephyr_library_include_directories(${ZEPHYR_BASE}/subsys/net/lib/sockets) +zephyr_library_sources( + sim7080.c + sim7080_pdp.c + sim7080_dns.c + sim7080_sock.c + sim7080_sms.c + sim7080_ftp.c + sim7080_gps.c + sim7080_meas.c + sim7080_utils.c) diff --git a/drivers/modem/Kconfig.simcom-sim7080 b/drivers/modem/simcom/sim7080/Kconfig similarity index 64% rename from drivers/modem/Kconfig.simcom-sim7080 rename to drivers/modem/simcom/sim7080/Kconfig index e8cee8f5fc12d..34bee179b47f0 100644 --- a/drivers/modem/Kconfig.simcom-sim7080 +++ b/drivers/modem/simcom/sim7080/Kconfig @@ -49,6 +49,28 @@ config MODEM_SIMCOM_SIM7080_APN context. This value is specific to the network provider and may need to be changed. +config MODEM_SIMCOM_SIM7080_BAUDRATE + int "Baudrate for modem UART" + default 115200 + help + Set this to the baudrate the uart is using. On first startup + the modem is configured to use autobaud. The driver will then + configure the modem to use a fixed baudrate for faster startups. + +config MODEM_SIMCOM_SIM7080_DNS_DEFAULT_RECOUNT + int "Retry count for a DNS query" + range 0 10 + default 10 + help + The number of retries for a DNS lookup + +config MODEM_SIMCOM_SIM7080_DNS_DEFAULT_TIMEOUT + int "Timeout for a DNS query" + range 0 60000 + default 20000 + help + The timeout for DNS queries in milliseconds + choice MODEM_SIMCOM_SIM7080_RAT bool "Radio Access Technology Mode" default MODEM_SIMCOM_SIM7080_RAT_NB1 @@ -70,4 +92,20 @@ config MODEM_SIMCOM_SIM7080_RAT_GSM endchoice +choice + bool "Modem Boot Type" + default MODEM_SIMCOM_SIM7080_BOOT_TYPE_NORMAL + +config MODEM_SIMCOM_SIM7080_BOOT_TYPE_NORMAL + bool "Normal boot" + help + Boot the modem and attach to network + +config MODEM_SIMCOM_SIM7080_BOOT_TYPE_CONSTRAINED + bool "Constrained boot" + help + Check if the modem is operational and then power off + +endchoice + endif # MODEM_SIM7080 diff --git a/drivers/modem/simcom/sim7080/sim7080.c b/drivers/modem/simcom/sim7080/sim7080.c new file mode 100644 index 0000000000000..d3cb6987dc2d5 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080.c @@ -0,0 +1,884 @@ +/* + * Copyright (C) 2021 metraTec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT simcom_sim7080 + +#include +#include +LOG_MODULE_REGISTER(modem_simcom_sim7080, CONFIG_MODEM_LOG_LEVEL); + +#include +#include "sim7080.h" + +struct sim7080_data mdata; +struct modem_context mctx; + +static struct k_thread modem_rx_thread; +struct k_work_q modem_workq; + +static K_KERNEL_STACK_DEFINE(modem_rx_stack, CONFIG_MODEM_SIMCOM_SIM7080_RX_STACK_SIZE); +static K_KERNEL_STACK_DEFINE(modem_workq_stack, CONFIG_MODEM_SIMCOM_SIM7080_RX_WORKQ_STACK_SIZE); +NET_BUF_POOL_DEFINE(mdm_recv_pool, MDM_RECV_MAX_BUF, MDM_RECV_BUF_SIZE, 0, NULL); + +/* pin settings */ +static const struct gpio_dt_spec power_gpio = GPIO_DT_SPEC_INST_GET(0, mdm_power_gpios); + +static inline uint32_t hash32(char *str, int len) +{ +#define HASH_MULTIPLIER 37 + uint32_t h = 0; + int i; + + for (i = 0; i < len; ++i) { + h = (h * HASH_MULTIPLIER) + str[i]; + } + + return h; +} + +static inline uint8_t *modem_get_mac(const struct device *dev) +{ + struct sim7080_data *data = dev->data; + uint32_t hash_value; + + data->mac_addr[0] = 0x00; + data->mac_addr[1] = 0x10; + + /* use IMEI for mac_addr */ + hash_value = hash32(mdata.mdm_imei, strlen(mdata.mdm_imei)); + + UNALIGNED_PUT(hash_value, (uint32_t *)(data->mac_addr + 2)); + + return data->mac_addr; +} + +/* Setup the Modem NET Interface. */ +static void modem_net_iface_init(struct net_if *iface) +{ + const struct device *dev = net_if_get_device(iface); + struct sim7080_data *data = dev->data; + + net_if_set_link_addr(iface, modem_get_mac(dev), sizeof(data->mac_addr), NET_LINK_ETHERNET); + + data->netif = iface; + + socket_offload_dns_register(&offload_dns_ops); + + net_if_socket_offload_set(iface, sim7080_offload_socket); +} + +/** + * Changes the operating state of the sim7080. + * + * @param state The new state. + */ +void sim7080_change_state(enum sim7080_state state) +{ + LOG_DBG("Changing state to (%d)", state); + mdata.state = state; +} + +/** + * Get the current operating state of the sim7080. + * + * @return The current state. + */ +enum sim7080_state sim7080_get_state(void) +{ + return mdata.state; +} + +enum sim7080_state mdm_sim7080_get_state(void) +{ + return sim7080_get_state(); +} + +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) +{ + if (family != AF_INET && + family != AF_INET6) { + return false; + } + + if (type != SOCK_DGRAM && + type != SOCK_STREAM) { + return false; + } + + if (proto != IPPROTO_TCP && + proto != IPPROTO_UDP) { + return false; + } + + return true; +} + +/* + * Process all messages received from the modem. + */ +static void modem_rx(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + while (true) { + /* Wait for incoming data */ + modem_iface_uart_rx_wait(&mctx.iface, K_FOREVER); + + modem_cmd_handler_process(&mctx.cmd_handler, &mctx.iface); + } +} + +int mdm_sim7080_set_gpio(int gpio, int level) +{ + int ret; + char buf[sizeof("AT+SGPIO=#,#,#,#")]; + + ret = snprintk(buf, sizeof(buf), "AT+SGPIO=0,%u,1,%u", gpio, !!level); + if (ret < 0) { + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0, buf, &mdata.sem_response, + K_SECONDS(5)); + if (ret < 0) { + return -1; + } + + return ret; +} + +MODEM_CMD_DEFINE(on_cmd_ok) +{ + modem_cmd_handler_set_error(data, 0); + k_sem_give(&mdata.sem_response); + return 0; +} + +MODEM_CMD_DEFINE(on_cmd_error) +{ + modem_cmd_handler_set_error(data, -EIO); + k_sem_give(&mdata.sem_response); + return 0; +} + +MODEM_CMD_DEFINE(on_cmd_exterror) +{ + modem_cmd_handler_set_error(data, -EIO); + k_sem_give(&mdata.sem_response); + return 0; +} + +/* + * Read manufacturer identification. + */ +MODEM_CMD_DEFINE(on_cmd_cgmi) +{ + size_t out_len = net_buf_linearize( + mdata.mdm_manufacturer, sizeof(mdata.mdm_manufacturer) - 1, data->rx_buf, 0, len); + mdata.mdm_manufacturer[out_len] = '\0'; + LOG_INF("Manufacturer: %s", mdata.mdm_manufacturer); + return 0; +} + +/* + * Read model identification. + */ +MODEM_CMD_DEFINE(on_cmd_cgmm) +{ + size_t out_len = net_buf_linearize(mdata.mdm_model, sizeof(mdata.mdm_model) - 1, + data->rx_buf, 0, len); + mdata.mdm_model[out_len] = '\0'; + LOG_INF("Model: %s", mdata.mdm_model); + return 0; +} + +/* + * Read software release. + * + * Response will be in format RESPONSE: . + */ +MODEM_CMD_DEFINE(on_cmd_cgmr) +{ + size_t out_len; + char *p; + + out_len = net_buf_linearize(mdata.mdm_revision, sizeof(mdata.mdm_revision) - 1, + data->rx_buf, 0, len); + mdata.mdm_revision[out_len] = '\0'; + + /* The module prepends a Revision: */ + p = strchr(mdata.mdm_revision, ':'); + if (p) { + out_len = strlen(p + 1); + memmove(mdata.mdm_revision, p + 1, out_len + 1); + } + + LOG_INF("Revision: %s", mdata.mdm_revision); + return 0; +} + +/* + * Read serial number identification. + */ +MODEM_CMD_DEFINE(on_cmd_cgsn) +{ + size_t out_len = + net_buf_linearize(mdata.mdm_imei, sizeof(mdata.mdm_imei) - 1, data->rx_buf, 0, len); + mdata.mdm_imei[out_len] = '\0'; + LOG_INF("IMEI: %s", mdata.mdm_imei); + return 0; +} + +#if defined(CONFIG_MODEM_SIM_NUMBERS) +/* + * Read international mobile subscriber identity. + */ +MODEM_CMD_DEFINE(on_cmd_cimi) +{ + size_t out_len = + net_buf_linearize(mdata.mdm_imsi, sizeof(mdata.mdm_imsi) - 1, data->rx_buf, 0, len); + mdata.mdm_imsi[out_len] = '\0'; + + /* Log the received information. */ + LOG_INF("IMSI: %s", mdata.mdm_imsi); + return 0; +} + +/* + * Read iccid. + */ +MODEM_CMD_DEFINE(on_cmd_ccid) +{ + size_t out_len = net_buf_linearize(mdata.mdm_iccid, sizeof(mdata.mdm_iccid) - 1, + data->rx_buf, 0, len); + mdata.mdm_iccid[out_len] = '\0'; + + /* Log the received information. */ + LOG_INF("ICCID: %s", mdata.mdm_iccid); + return 0; +} +#endif /* defined(CONFIG_MODEM_SIM_NUMBERS) */ + +/* + * Unlock the tx ready semaphore if '> ' is received. + */ +MODEM_CMD_DIRECT_DEFINE(on_cmd_tx_ready) +{ + k_sem_give(&mdata.sem_tx_ready); + return len; +} + +/* + * Handles pdp context urc. + * + * The urc has the form +APP PDP: ,. + * When activated ACTIVE is reported as state. + * All other states will be treated as deactivated. + */ +MODEM_CMD_DEFINE(on_urc_app_pdp) +{ + bool active = strcmp(argv[1], "ACTIVE") == 0; + + if (active) { + mdata.status_flags |= SIM7080_STATUS_FLAG_PDP_ACTIVE; + } else { + mdata.status_flags &= ~SIM7080_STATUS_FLAG_PDP_ACTIVE; + } + + LOG_INF("PDP context: %u", active); + k_sem_give(&mdata.pdp_sem); + return 0; +} + +MODEM_CMD_DEFINE(on_urc_pdp_deact) +{ + LOG_INF("PDP context deactivated by network"); + + mdata.status_flags &= ~SIM7080_STATUS_FLAG_PDP_ACTIVE; + return 0; +} + +MODEM_CMD_DEFINE(on_urc_sms) +{ + LOG_INF("SMS: %s", argv[0]); + return 0; +} + +/* + * Handles socket data notification. + * + * The sim modem sends and unsolicited +CADATAIND: + * if data can be read from a socket. + */ +MODEM_CMD_DEFINE(on_urc_cadataind) +{ + int sock_fd; + + sock_fd = atoi(argv[0]); + + sim7080_handle_sock_data_indication(sock_fd); + return 0; +} + +/* + * Handles the castate response. + * + * +CASTATE: , + * + * Cid is the connection id (socket fd) and + * state can be: + * 0 - Closed by remote server or error + * 1 - Connected to remote server + * 2 - Listening + */ +MODEM_CMD_DEFINE(on_urc_castate) +{ + int sockfd, state; + + sockfd = atoi(argv[0]); + state = atoi(argv[1]); + + sim7080_handle_sock_state(sockfd, state); + return 0; +} + +/** + * Handles the ftpget urc. + * + * +FTPGET: , + * + * Mode can be 1 for opening a session and + * reporting that data is available or 2 for + * reading data. This urc handler will only handle + * mode 1 because 2 will not occur as urc. + * + * Error can be either: + * - 1 for data available/opened session. + * - 0 If transfer is finished. + * - >0 for some error. + */ +MODEM_CMD_DEFINE(on_urc_ftpget) +{ + int error = atoi(argv[0]); + + LOG_INF("+FTPGET: 1,%d", error); + + /* Transfer finished. */ + if (error == 0) { + mdata.ftp.state = SIM7080_FTP_CONNECTION_STATE_FINISHED; + } else if (error == 1) { + mdata.ftp.state = SIM7080_FTP_CONNECTION_STATE_CONNECTED; + } else { + mdata.ftp.state = SIM7080_FTP_CONNECTION_STATE_ERROR; + } + + k_sem_give(&mdata.sem_ftp); + + return 0; +} + +MODEM_CMD_DIRECT_DEFINE(on_urc_rdy) +{ + LOG_DBG("RDY received"); + mdata.status_flags |= SIM7080_STATUS_FLAG_POWER_ON; + k_sem_give(&mdata.boot_sem); + return 0; +} + +MODEM_CMD_DIRECT_DEFINE(on_urc_pwr_down) +{ + LOG_DBG("POWER DOWN received"); + mdata.status_flags &= ~SIM7080_STATUS_FLAG_POWER_ON; + k_sem_give(&mdata.boot_sem); + return 0; +} + +MODEM_CMD_DEFINE(on_urc_cpin) +{ + if (strcmp(argv[0], "READY") == 0) { + mdata.status_flags |= SIM7080_STATUS_FLAG_CPIN_READY; + } else { + mdata.status_flags &= ~SIM7080_STATUS_FLAG_CPIN_READY; + } + + k_sem_give(&mdata.boot_sem); + + LOG_INF("CPIN: %s", argv[0]); + return 0; +} + +MODEM_CMD_DEFINE(on_urc_httptofs) +{ + mdata.http_status = (uint16_t)strtoul(argv[0], NULL, 10); + LOG_INF("HTTP status: %u", mdata.http_status); + k_sem_give(&mdata.sem_http); + return 0; +} + +/* + * Possible responses by the sim7080. + */ +static const struct modem_cmd response_cmds[] = { + MODEM_CMD("OK", on_cmd_ok, 0U, ""), + MODEM_CMD("ERROR", on_cmd_error, 0U, ""), + MODEM_CMD("+CME ERROR: ", on_cmd_exterror, 1U, ""), + MODEM_CMD_DIRECT(">", on_cmd_tx_ready), +}; + +/* + * Possible unsolicited commands. + */ +static const struct modem_cmd unsolicited_cmds[] = { + MODEM_CMD("+APP PDP: ", on_urc_app_pdp, 2U, ","), + MODEM_CMD("+PDP: DEACT", on_urc_pdp_deact, 0U, ""), + MODEM_CMD("SMS ", on_urc_sms, 1U, ""), + MODEM_CMD("+CADATAIND: ", on_urc_cadataind, 1U, ""), + MODEM_CMD("+CASTATE: ", on_urc_castate, 2U, ","), + MODEM_CMD("+FTPGET: 1,", on_urc_ftpget, 1U, ""), + MODEM_CMD("RDY", on_urc_rdy, 0U, ""), + MODEM_CMD("NORMAL POWER DOWN", on_urc_pwr_down, 0U, ""), + MODEM_CMD("+CPIN: ", on_urc_cpin, 1U, ","), + MODEM_CMD("+HTTPTOFS: ", on_urc_httptofs, 2U, ","), +}; + +/* + * Toggles the modems power pin. + */ +static void modem_pwrkey(void) +{ + LOG_DBG("Pulling PWRKEY"); + /* Power pin should be high for 1.5 seconds. */ + gpio_pin_set_dt(&power_gpio, 1); + k_sleep(K_MSEC(1500)); + gpio_pin_set_dt(&power_gpio, 0); +} + +static int modem_set_baudrate(uint32_t baudrate) +{ + char buf[sizeof("AT+IPR=##########")] = {0}; + + int ret = snprintk(buf, sizeof(buf), "AT+IPR=%u", baudrate); + + if (ret < 0) { + LOG_ERR("Failed to build command"); + goto out; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buf, &mdata.sem_response, + K_SECONDS(2)); + if (ret != 0) { + LOG_ERR("Failed to set baudrate"); + } + +out: + return ret; +} + +/** + * Performs the autobaud sequence until modem answers or limit is reached. + * + * @return On successful boot 0 is returned. Otherwise <0 is returned. + */ +int modem_autobaud(void) +{ + int counter = 0; + int ret = -1; + + /* + * The sim7080 has a autobaud function. + * On startup multiple AT's are sent until + * a OK is received. + */ + counter = 0; + while (counter++ <= MDM_MAX_AUTOBAUD) { + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT", + &mdata.sem_response, K_MSEC(500)); + if (ret != 0) { + LOG_DBG("No response to autobaud AT"); + continue; + } + break; + } + + return ret; +} + +/** + * Power on the modem and wait for operational sim card. + * + * @param allow_autobaud Allow autobaud functionality. + * @return 0 on success. Otherwise <0. + * + * @note Autobaud is only allowed during driver setup. + * In any other case a fixed baudrate should be used. + */ +static int modem_boot(bool allow_autobaud) +{ + uint8_t boot_tries = 0; + int ret = -1; + + /* Reset the status flags */ + mdata.status_flags = 0; + + /* Try boot multiple times in case modem was already on */ + while (boot_tries++ <= MDM_BOOT_TRIES) { + + k_sem_reset(&mdata.boot_sem); + + modem_pwrkey(); + + ret = k_sem_take(&mdata.boot_sem, K_SECONDS(5)); + if (ret == 0) { + if (mdata.status_flags & SIM7080_STATUS_FLAG_POWER_ON) { + LOG_INF("Modem booted"); + break; + } + + LOG_INF("Modem turned off"); + k_sleep(K_SECONDS(1)); + continue; + } + + LOG_WRN("No modem response after pwrkey"); + + if (allow_autobaud == false) { + continue; + } + + LOG_INF("Trying autobaud"); + + ret = modem_autobaud(); + if (ret != 0) { + LOG_WRN("Autobaud failed"); + continue; + } + + /* Set baudrate to disable autobaud on next startup */ + ret = modem_set_baudrate(CONFIG_MODEM_SIMCOM_SIM7080_BAUDRATE); + if (ret != 0) { + LOG_ERR("Failed to disable echo"); + continue; + } + + /* Reset modem and wait for ready indication */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+CFUN=1,1", + &mdata.sem_response, K_MSEC(500)); + if (ret != 0) { + LOG_ERR("Reset failed"); + break; + } + + ret = k_sem_take(&mdata.boot_sem, K_SECONDS(5)); + if (ret != 0) { + LOG_ERR("No RDY received!"); + break; + } + + if ((mdata.status_flags & SIM7080_STATUS_FLAG_POWER_ON) == 0) { + LOG_ERR("Modem not powered"); + break; + } + + break; + } + + if (ret != 0) { + LOG_ERR("Modem boot failed!"); + goto out; + } + + /* Wait for sim card status */ + ret = k_sem_take(&mdata.boot_sem, K_SECONDS(5)); + if (ret != 0) { + LOG_ERR("Timeout while waiting for sim status"); + goto out; + } + + if ((mdata.status_flags & SIM7080_STATUS_FLAG_CPIN_READY) == 0) { + LOG_ERR("Sim card not ready!"); + goto out; + } + + /* Disable echo on successful boot */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "ATE0", + &mdata.sem_response, K_MSEC(500)); + if (ret != 0) { + LOG_ERR("Disabling echo failed"); + goto out; + } + + sim7080_change_state(SIM7080_STATE_IDLE); + +out: + return ret; +} + +/* + * Commands to be sent at setup. + */ +static const struct setup_cmd setup_cmds[] = { + SETUP_CMD("AT+CGMI", "", on_cmd_cgmi, 0U, ""), + SETUP_CMD("AT+CGMM", "", on_cmd_cgmm, 0U, ""), + SETUP_CMD("AT+CGMR", "", on_cmd_cgmr, 0U, ""), + SETUP_CMD("AT+CGSN", "", on_cmd_cgsn, 0U, ""), +#if defined(CONFIG_MODEM_SIM_NUMBERS) + SETUP_CMD("AT+CIMI", "", on_cmd_cimi, 0U, ""), + SETUP_CMD("AT+CCID", "", on_cmd_ccid, 0U, ""), +#endif /* defined(CONFIG_MODEM_SIM_NUMBERS) */ +}; + +/* + * Does the modem setup by starting it and + * bringing the modem to a PDP active state. + */ +static int modem_setup(void) +{ + int ret = 0; + + k_work_cancel_delayable(&mdata.rssi_query_work); + + ret = modem_boot(true); + if (ret < 0) { + LOG_ERR("Booting modem failed!!"); + return ret; + } + + ret = modem_cmd_handler_setup_cmds(&mctx.iface, &mctx.cmd_handler, setup_cmds, + ARRAY_SIZE(setup_cmds), &mdata.sem_response, + MDM_REGISTRATION_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to send init commands!"); + return ret; + } + + if (strcmp(mdata.mdm_model, "SIMCOM_SIM7080") != 0) { + LOG_ERR("Wrong modem model: %s", mdata.mdm_model); + ret = -EINVAL; + return ret; + } + +#if IS_ENABLED(CONFIG_MODEM_SIMCOM_SIM7080_BOOT_TYPE_CONSTRAINED) + ret = mdm_sim7080_power_off(); +#elif IS_ENABLED(CONFIG_MODEM_SIMCOM_SIM7080_BOOT_TYPE_NORMAL) + ret = sim7080_pdp_activate(); +#else +#error No boot type selected +#endif + + return ret; +} + +int mdm_sim7080_power_on(void) +{ + return modem_boot(false); +} + +int mdm_sim7080_power_off(void) +{ + int ret = -EALREADY; + + k_work_cancel_delayable(&mdata.rssi_query_work); + + if ((mdata.status_flags & SIM7080_STATUS_FLAG_POWER_ON) == 0) { + LOG_WRN("Modem already off"); + goto out; + } + + k_sem_reset(&mdata.boot_sem); + + /* Pull pwrkey to turn off */ + modem_pwrkey(); + + /* Wait for power down indication */ + ret = k_sem_take(&mdata.boot_sem, K_SECONDS(5)); + if (ret != 0) { + LOG_ERR("No power down indication"); + goto out; + } + + if ((mdata.status_flags & SIM7080_STATUS_FLAG_POWER_ON) != 0) { + LOG_ERR("Modem not powered down!"); + ret = -1; + goto out; + } + + LOG_DBG("Modem turned off"); + mdata.status_flags = 0; + sim7080_change_state(SIM7080_STATE_OFF); + +out: + return ret; +} + +void mdm_sim7080_force_reset(void) +{ + LOG_DBG("Forcefully resetting modem"); + gpio_pin_set_dt(&power_gpio, 1); + k_sleep(K_SECONDS(15)); + gpio_pin_set_dt(&power_gpio, 0); +} + +const char *mdm_sim7080_get_manufacturer(void) +{ + return mdata.mdm_manufacturer; +} + +const char *mdm_sim7080_get_model(void) +{ + return mdata.mdm_model; +} + +const char *mdm_sim7080_get_revision(void) +{ + return mdata.mdm_revision; +} + +const char *mdm_sim7080_get_imei(void) +{ + return mdata.mdm_imei; +} + +#if defined(CONFIG_MODEM_SIM_NUMBERS) +const char *mdm_sim7080_get_iccid(void) +{ + return mdata.mdm_iccid; +} +#else +const char *mdm_sim7080_get_iccid(void) +{ + return NULL; +} +#endif /* #if defined(CONFIG_MODEM_SIM_NUMBERS) */ + +/* + * Initializes modem handlers and context. + * After successful init this function calls + * modem_setup. + */ +static int modem_init(const struct device *dev) +{ + int ret; + + ARG_UNUSED(dev); + + k_sem_init(&mdata.sem_response, 0, 1); + k_sem_init(&mdata.sem_tx_ready, 0, 1); + k_sem_init(&mdata.sem_dns, 0, 1); + k_sem_init(&mdata.sem_ftp, 0, 1); + k_sem_init(&mdata.sem_http, 0, 1); + k_sem_init(&mdata.boot_sem, 0, 1); + k_sem_init(&mdata.pdp_sem, 0, 1); + k_work_queue_start(&modem_workq, modem_workq_stack, + K_KERNEL_STACK_SIZEOF(modem_workq_stack), K_PRIO_COOP(7), NULL); + + /* Assume the modem is not registered to the network. */ + mdata.mdm_registration = 0; + mdata.status_flags = 0; + + mdata.sms_buffer = NULL; + mdata.sms_buffer_pos = 0; + + /* Socket config. */ + ret = modem_socket_init(&mdata.socket_config, &mdata.sockets[0], ARRAY_SIZE(mdata.sockets), + MDM_BASE_SOCKET_NUM, true, &offload_socket_fd_op_vtable); + if (ret < 0) { + goto error; + } + + sim7080_change_state(SIM7080_STATE_OFF); + + /* Command handler. */ + const struct modem_cmd_handler_config cmd_handler_config = { + .match_buf = &mdata.cmd_match_buf[0], + .match_buf_len = sizeof(mdata.cmd_match_buf), + .buf_pool = &mdm_recv_pool, + .alloc_timeout = BUF_ALLOC_TIMEOUT, + .eol = "\r\n", + .user_data = NULL, + .response_cmds = response_cmds, + .response_cmds_len = ARRAY_SIZE(response_cmds), + .unsol_cmds = unsolicited_cmds, + .unsol_cmds_len = ARRAY_SIZE(unsolicited_cmds), + }; + + ret = modem_cmd_handler_init(&mctx.cmd_handler, &mdata.cmd_handler_data, + &cmd_handler_config); + if (ret < 0) { + goto error; + } + + /* Uart handler. */ + const struct modem_iface_uart_config uart_config = { + .rx_rb_buf = &mdata.iface_rb_buf[0], + .rx_rb_buf_len = sizeof(mdata.iface_rb_buf), + .dev = MDM_UART_DEV, + .hw_flow_control = DT_PROP(MDM_UART_NODE, hw_flow_control), + }; + + ret = modem_iface_uart_init(&mctx.iface, &mdata.iface_data, &uart_config); + if (ret < 0) { + goto error; + } + + mdata.current_sock_fd = -1; + mdata.current_sock_written = 0; + + mdata.dns.recount = CONFIG_MODEM_SIMCOM_SIM7080_DNS_DEFAULT_RECOUNT; + mdata.dns.timeout = CONFIG_MODEM_SIMCOM_SIM7080_DNS_DEFAULT_TIMEOUT; + + mdata.ftp.read_buffer = NULL; + mdata.ftp.nread = 0; + mdata.ftp.state = SIM7080_FTP_CONNECTION_STATE_INITIAL; + + /* Modem data storage. */ + mctx.data_manufacturer = mdata.mdm_manufacturer; + mctx.data_model = mdata.mdm_model; + mctx.data_revision = mdata.mdm_revision; + mctx.data_imei = mdata.mdm_imei; +#if defined(CONFIG_MODEM_SIM_NUMBERS) + mctx.data_imsi = mdata.mdm_imsi; + mctx.data_iccid = mdata.mdm_iccid; +#endif /* #if defined(CONFIG_MODEM_SIM_NUMBERS) */ + mctx.data_rssi = &mdata.mdm_rssi; + + ret = gpio_pin_configure_dt(&power_gpio, GPIO_OUTPUT_LOW); + if (ret < 0) { + LOG_ERR("Failed to configure %s pin", "power"); + goto error; + } + + mctx.driver_data = &mdata; + + ret = modem_context_register(&mctx); + if (ret < 0) { + LOG_ERR("Error registering modem context: %d", ret); + goto error; + } + + k_thread_create(&modem_rx_thread, modem_rx_stack, K_KERNEL_STACK_SIZEOF(modem_rx_stack), + modem_rx, NULL, NULL, NULL, K_PRIO_COOP(7), 0, K_NO_WAIT); + + /* Init RSSI query */ + k_work_init_delayable(&mdata.rssi_query_work, sim7080_rssi_query_work); + + return modem_setup(); +error: + return ret; +} + +/* Register device with the networking stack. */ +NET_DEVICE_DT_INST_OFFLOAD_DEFINE(0, modem_init, NULL, &mdata, NULL, + CONFIG_MODEM_SIMCOM_SIM7080_INIT_PRIORITY, &api_funcs, + MDM_MAX_DATA_LENGTH); + +NET_SOCKET_OFFLOAD_REGISTER(simcom_sim7080, CONFIG_NET_SOCKETS_OFFLOAD_PRIORITY, + AF_UNSPEC, offload_is_supported, sim7080_offload_socket); diff --git a/drivers/modem/simcom-sim7080.h b/drivers/modem/simcom/sim7080/sim7080.h similarity index 75% rename from drivers/modem/simcom-sim7080.h rename to drivers/modem/simcom/sim7080/sim7080.h index cb16100cb5857..57ca6c6e8a074 100644 --- a/drivers/modem/simcom-sim7080.h +++ b/drivers/modem/simcom/sim7080/sim7080.h @@ -45,7 +45,7 @@ #define MDM_MAX_AUTOBAUD 5 #define MDM_MAX_CEREG_WAITS 40 #define MDM_MAX_CGATT_WAITS 40 -#define MDM_BOOT_TRIES 4 +#define MDM_BOOT_TRIES 2 #define MDM_GNSS_PARSER_MAX_LEN 128 #define MDM_APN CONFIG_MODEM_SIMCOM_SIM7080_APN #define MDM_LTE_BANDS CONFIG_MODEM_SIMCOM_SIM7080_LTE_BANDS @@ -61,13 +61,6 @@ #define MDM_IMSI_LENGTH 16 #define MDM_ICCID_LENGTH 32 -enum sim7080_state { - SIM7080_STATE_INIT = 0, - SIM7080_STATE_NETWORKING, - SIM7080_STATE_GNSS, - SIM7080_STATE_OFF, -}; - /* Possible states of the ftp connection. */ enum sim7080_ftp_connection_state { /* Not connected yet. */ @@ -80,6 +73,13 @@ enum sim7080_ftp_connection_state { SIM7080_FTP_CONNECTION_STATE_ERROR, }; +enum sim7080_status_flags { + SIM7080_STATUS_FLAG_POWER_ON = 0x01, + SIM7080_STATUS_FLAG_CPIN_READY = 0x02, + SIM7080_STATUS_FLAG_ATTACHED = 0x04, + SIM7080_STATUS_FLAG_PDP_ACTIVE = 0x08, +}; + /* * Driver data. */ @@ -129,26 +129,27 @@ struct sim7080_data { */ int current_sock_fd; int current_sock_written; + size_t tx_space_avail; + uint8_t socket_open_rc; /* * Network registration of the modem. */ uint8_t mdm_registration; - /* - * Whether gprs is attached or detached. - */ - uint8_t mdm_cgatt; - /* - * If the sim card is ready or not. - */ - bool cpin_ready; - /* - * Flag if the PDP context is active. - */ - bool pdp_active; + /* Modem status flags */ + uint32_t status_flags; /* SMS buffer structure provided by read. */ struct sim7080_sms_buffer *sms_buffer; /* Position in the sms buffer. */ uint8_t sms_buffer_pos; + /* Status of the last http operation */ + uint16_t http_status; + /* DNS related variables */ + struct { + /* Number of DNS retries */ + uint8_t recount; + /* Timeout in milliseconds */ + uint16_t timeout; + } dns; /* Ftp related variables. */ struct { /* User buffer for ftp data. */ @@ -165,6 +166,9 @@ struct sim7080_data { struct k_sem sem_tx_ready; struct k_sem sem_dns; struct k_sem sem_ftp; + struct k_sem sem_http; + struct k_sem boot_sem; + struct k_sem pdp_sem; }; /* @@ -177,4 +181,30 @@ struct socket_read_data { uint16_t recv_read_len; }; +/* + * Driver internals + */ +extern struct sim7080_data mdata; +extern struct modem_context mctx; +extern const struct socket_op_vtable offload_socket_fd_op_vtable; +extern const struct socket_dns_offload offload_dns_ops; +extern struct k_work_q modem_workq; + +extern void sim7080_rssi_query_work(struct k_work *work); + +enum sim7080_state sim7080_get_state(void); + +void sim7080_change_state(enum sim7080_state state); + +int sim7080_pdp_activate(void); +int sim7080_pdp_deactivate(void); + +int sim7080_offload_socket(int family, int type, int proto); + +void sim7080_handle_sock_data_indication(int fd); + +void sim7080_handle_sock_state(int fd, uint8_t state); + +int sim7080_utils_parse_time(uint8_t *date, uint8_t *time_str, struct tm *t); + #endif /* SIMCOM_SIM7080_H */ diff --git a/drivers/modem/simcom/sim7080/sim7080_dns.c b/drivers/modem/simcom/sim7080/sim7080_dns.c new file mode 100644 index 0000000000000..105de0ce3b294 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_dns.c @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2025 metraTec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT simcom_sim7080 + +#include +#include +LOG_MODULE_REGISTER(modem_simcom_sim7080_dns, CONFIG_MODEM_LOG_LEVEL); + +#include +#include "sim7080.h" + +static struct zsock_addrinfo dns_result; +static struct sockaddr dns_result_addr; +static char dns_result_canonname[DNS_MAX_NAME_SIZE + 1]; + +/* + * Parses the dns response from the modem. + * + * Response on success: + * +CDNSGIP: 1,,[,] + * + * Response on failure: + * +CDNSGIP: 0, + */ +MODEM_CMD_DEFINE(on_cmd_cdnsgip) +{ + int state; + char ips[256]; + size_t out_len; + int ret = -1; + + state = atoi(argv[0]); + if (state == 0) { + LOG_ERR("DNS lookup failed with error %s", argv[1]); + goto exit; + } + + /* Offset to skip the leading " */ + out_len = net_buf_linearize(ips, sizeof(ips) - 1, data->rx_buf, 1, len); + ips[out_len] = '\0'; + + /* find trailing " */ + char *ipv4 = strstr(ips, "\""); + + if (!ipv4) { + LOG_ERR("Malformed DNS response!!"); + goto exit; + } + + *ipv4 = '\0'; + net_addr_pton(dns_result.ai_family, ips, + &((struct sockaddr_in *)&dns_result_addr)->sin_addr); + ret = 0; + +exit: + k_sem_give(&mdata.sem_dns); + return ret; +} + +/* + * Perform a dns lookup. + */ +static int offload_getaddrinfo(const char *node, const char *service, + const struct zsock_addrinfo *hints, struct zsock_addrinfo **res) +{ + struct modem_cmd cmd[] = { MODEM_CMD("+CDNSGIP: ", on_cmd_cdnsgip, 2U, ",") }; + char sendbuf[sizeof("AT+CDNSGIP=\"\",##,#####") + 128]; + uint32_t port = 0; + int ret; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_ERR("Modem currently not attached to the network!"); + return DNS_EAI_AGAIN; + } + + /* init result */ + (void)memset(&dns_result, 0, sizeof(dns_result)); + (void)memset(&dns_result_addr, 0, sizeof(dns_result_addr)); + + /* Currently only support IPv4. */ + dns_result.ai_family = AF_INET; + dns_result_addr.sa_family = AF_INET; + dns_result.ai_addr = &dns_result_addr; + dns_result.ai_addrlen = sizeof(dns_result_addr); + dns_result.ai_canonname = dns_result_canonname; + dns_result_canonname[0] = '\0'; + + if (service) { + port = atoi(service); + if (port < 1 || port > USHRT_MAX) { + return DNS_EAI_SERVICE; + } + } + + if (port > 0U) { + if (dns_result.ai_family == AF_INET) { + net_sin(&dns_result_addr)->sin_port = htons(port); + } + } + + /* Check if node is an IP address */ + if (net_addr_pton(dns_result.ai_family, node, + &((struct sockaddr_in *)&dns_result_addr)->sin_addr) == 0) { + *res = &dns_result; + return 0; + } + + /* user flagged node as numeric host, but we failed net_addr_pton */ + if (hints && hints->ai_flags & AI_NUMERICHOST) { + return DNS_EAI_NONAME; + } + + ret = snprintk(sendbuf, sizeof(sendbuf), "AT+CDNSGIP=\"%s\",%u,%u", node, + mdata.dns.recount, mdata.dns.timeout); + if (ret < 0) { + LOG_ERR("Formatting dns query failed"); + return ret; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmd, ARRAY_SIZE(cmd), sendbuf, + &mdata.sem_dns, MDM_DNS_TIMEOUT); + if (ret < 0) { + return ret; + } + + *res = (struct zsock_addrinfo *)&dns_result; + return 0; +} + +/* + * Free addrinfo structure. + */ +static void offload_freeaddrinfo(struct zsock_addrinfo *res) +{ + /* No need to free static memory. */ + ARG_UNUSED(res); +} + +/* + * DNS vtable. + */ +const struct socket_dns_offload offload_dns_ops = { + .getaddrinfo = offload_getaddrinfo, + .freeaddrinfo = offload_freeaddrinfo, +}; + +int mdm_sim7080_dns_set_lookup_params(uint8_t recount, uint16_t timeout) +{ + if (recount > SIM7080_DNS_MAX_RECOUNT || timeout > SIM7080_DNS_MAX_TIMEOUT_MS) { + return -EINVAL; + } + + mdata.dns.recount = recount; + mdata.dns.timeout = timeout; + + return 0; +} + +void mdm_sim7080_dns_get_lookup_params(uint8_t *recount, uint16_t *timeout) +{ + if (recount) { + *recount = mdata.dns.recount; + } + + if (timeout) { + *timeout = mdata.dns.timeout; + } +} diff --git a/drivers/modem/simcom/sim7080/sim7080_ftp.c b/drivers/modem/simcom/sim7080/sim7080_ftp.c new file mode 100644 index 0000000000000..01bab067337d1 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_ftp.c @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2025 Metratec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(modem_sim7080_ftp, CONFIG_MODEM_LOG_LEVEL); + +#include "sim7080.h" + +/** + * Parse the +FTPGET response. + * + * +FTPGET: , + * + * Mode is hard set to 2. + * + * Length is the number of bytes following (the ftp data). + */ +MODEM_CMD_DEFINE(on_cmd_ftpget) +{ + int nbytes = atoi(argv[0]); + int bytes_to_skip; + size_t out_len; + + if (nbytes == 0) { + mdata.ftp.nread = 0; + return 0; + } + + /* Skip length parameter and trailing \r\n */ + bytes_to_skip = strlen(argv[0]) + 2; + + /* Wait until data is ready. + * >= to ensure buffer is not empty after skip. + */ + if (net_buf_frags_len(data->rx_buf) <= nbytes + bytes_to_skip) { + return -EAGAIN; + } + + out_len = net_buf_linearize(mdata.ftp.read_buffer, mdata.ftp.nread, data->rx_buf, + bytes_to_skip, nbytes); + if (out_len != nbytes) { + LOG_WRN("FTP read size differs!"); + } + data->rx_buf = net_buf_skip(data->rx_buf, nbytes + bytes_to_skip); + + mdata.ftp.nread = nbytes; + + return 0; +} + +int mdm_sim7080_ftp_get_read(char *dst, size_t *size) +{ + int ret; + char buffer[sizeof("AT+FTPGET=#,######")]; + struct modem_cmd cmds[] = { MODEM_CMD("+FTPGET: 2,", on_cmd_ftpget, 1U, "") }; + + /* Some error occurred. */ + if (mdata.ftp.state == SIM7080_FTP_CONNECTION_STATE_ERROR || + mdata.ftp.state == SIM7080_FTP_CONNECTION_STATE_INITIAL) { + return SIM7080_FTP_RC_ERROR; + } + + /* Setup buffer. */ + mdata.ftp.read_buffer = dst; + mdata.ftp.nread = *size; + + /* Read ftp data. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPGET=2,%zu", *size); + if (ret < 0) { + *size = 0; + return SIM7080_FTP_RC_ERROR; + } + + /* Wait for data from the server. */ + k_sem_take(&mdata.sem_ftp, K_MSEC(200)); + + if (mdata.ftp.state == SIM7080_FTP_CONNECTION_STATE_FINISHED) { + *size = 0; + return SIM7080_FTP_RC_FINISHED; + } else if (mdata.ftp.state == SIM7080_FTP_CONNECTION_STATE_ERROR) { + *size = 0; + return SIM7080_FTP_RC_ERROR; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), buffer, + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + *size = 0; + return SIM7080_FTP_RC_ERROR; + } + + /* Set read size. */ + *size = mdata.ftp.nread; + + return SIM7080_FTP_RC_OK; +} + +int mdm_sim7080_ftp_get_start(const char *server, const char *user, const char *passwd, + const char *file, const char *path) +{ + int ret; + char buffer[256]; + + /* Start network. */ + ret = mdm_sim7080_start_network(); + if (ret < 0 && ret != -EALREADY) { + LOG_ERR("Failed to start network for FTP!"); + return -1; + } + + /* Set connection id for ftp. */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+FTPCID=0", + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set FTP Cid!"); + return -1; + } + + /* Set ftp server. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPSERV=\"%s\"", server); + if (ret < 0) { + LOG_WRN("Failed to build command!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buffer, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set FTP Cid!"); + return -1; + } + + /* Set ftp user. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPUN=\"%s\"", user); + if (ret < 0) { + LOG_WRN("Failed to build command!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buffer, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set ftp user!"); + return -1; + } + + /* Set ftp password. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPPW=\"%s\"", passwd); + if (ret < 0) { + LOG_WRN("Failed to build command!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buffer, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set ftp password!"); + return -1; + } + + /* Set ftp filename. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPGETNAME=\"%s\"", file); + if (ret < 0) { + LOG_WRN("Failed to build command!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buffer, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set ftp filename!"); + return -1; + } + + /* Set ftp filename. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPGETNAME=\"%s\"", file); + if (ret < 0) { + LOG_WRN("Failed to build command!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buffer, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set ftp filename!"); + return -1; + } + + /* Set ftp path. */ + ret = snprintk(buffer, sizeof(buffer), "AT+FTPGETPATH=\"%s\"", path); + if (ret < 0) { + LOG_WRN("Failed to build command!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buffer, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to set ftp path!"); + return -1; + } + + /* Initialize ftp variables. */ + mdata.ftp.read_buffer = NULL; + mdata.ftp.nread = 0; + mdata.ftp.state = SIM7080_FTP_CONNECTION_STATE_INITIAL; + + /* Start the ftp session. */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+FTPGET=1", + &mdata.sem_ftp, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_WRN("Failed to start session!"); + return -1; + } + + if (mdata.ftp.state != SIM7080_FTP_CONNECTION_STATE_CONNECTED) { + LOG_WRN("Session state is not connected!"); + return -1; + } + + return 0; +} diff --git a/drivers/modem/simcom/sim7080/sim7080_gps.c b/drivers/modem/simcom/sim7080/sim7080_gps.c new file mode 100644 index 0000000000000..86bfc9ef34004 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_gps.c @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2025 Metratec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(modem_sim7080_gps, CONFIG_MODEM_LOG_LEVEL); + +#include "sim7080.h" + +static struct sim7080_gnss_data gnss_data; + +/** + * Get the next parameter from the gnss phrase. + * + * @param src The source string supported on first call. + * @param delim The delimiter of the parameter list. + * @param saveptr Pointer for subsequent parses. + * @return On success a pointer to the parameter. On failure + * or end of string NULL is returned. + * + * This function is used instead of strtok because strtok would + * skip empty parameters, which is not desired. The modem may + * omit parameters which could lead to a incorrect parse. + */ +static char *gnss_get_next_param(char *src, const char *delim, char **saveptr) +{ + char *start, *del; + + if (src) { + start = src; + } else { + start = *saveptr; + } + + /* Illegal start string. */ + if (!start) { + return NULL; + } + + /* End of string reached. */ + if (*start == '\0' || *start == '\r') { + return NULL; + } + + del = strstr(start, delim); + if (!del) { + return NULL; + } + + *del = '\0'; + *saveptr = del + 1; + + if (del == start) { + return NULL; + } + + return start; +} + +static void gnss_skip_param(char **saveptr) +{ + gnss_get_next_param(NULL, ",", saveptr); +} + +/** + * Splits float parameters of the CGNSINF response on '.' + * + * @param src Null terminated string containing the float. + * @param f1 Resulting number part of the float. + * @param f2 Resulting fraction part of the float. + * @return 0 if parsing was successful. Otherwise <0 is returned. + * + * If the number part of the float is negative f1 and f2 will be + * negative too. + */ +static int gnss_split_on_dot(const char *src, int32_t *f1, int32_t *f2) +{ + char *dot = strchr(src, '.'); + + if (!dot) { + return -1; + } + + *dot = '\0'; + + *f1 = (int32_t)strtol(src, NULL, 10); + *f2 = (int32_t)strtol(dot + 1, NULL, 10); + + if (*f1 < 0) { + *f2 = -*f2; + } + + return 0; +} + +/** + * Parses cgnsinf response into the gnss_data structure. + * + * @param gps_buf Null terminated buffer containing the response. + * @return 0 on successful parse. Otherwise <0 is returned. + */ +static int parse_cgnsinf(char *gps_buf) +{ + char *saveptr; + int ret; + int32_t number, fraction; + + char *run_status = gnss_get_next_param(gps_buf, ",", &saveptr); + + if (run_status == NULL) { + goto error; + } else if (*run_status != '1') { + goto error; + } + + char *fix_status = gnss_get_next_param(NULL, ",", &saveptr); + + if (fix_status == NULL) { + goto error; + } else if (*fix_status != '1') { + goto error; + } + + char *utc = gnss_get_next_param(NULL, ",", &saveptr); + + if (utc == NULL) { + goto error; + } + + char *lat = gnss_get_next_param(NULL, ",", &saveptr); + + if (lat == NULL) { + goto error; + } + + char *lon = gnss_get_next_param(NULL, ",", &saveptr); + + if (lon == NULL) { + goto error; + } + + char *alt = gnss_get_next_param(NULL, ",", &saveptr); + char *speed = gnss_get_next_param(NULL, ",", &saveptr); + char *course = gnss_get_next_param(NULL, ",", &saveptr); + + /* discard fix mode and reserved*/ + gnss_skip_param(&saveptr); + gnss_skip_param(&saveptr); + + char *hdop = gnss_get_next_param(NULL, ",", &saveptr); + + if (hdop == NULL) { + goto error; + } + + gnss_data.run_status = 1; + gnss_data.fix_status = 1; + + strncpy(gnss_data.utc, utc, sizeof(gnss_data.utc) - 1); + + ret = gnss_split_on_dot(lat, &number, &fraction); + if (ret != 0) { + goto error; + } + gnss_data.lat = number * 10000000 + fraction * 10; + + ret = gnss_split_on_dot(lon, &number, &fraction); + if (ret != 0) { + goto error; + } + gnss_data.lon = number * 10000000 + fraction * 10; + + if (alt) { + ret = gnss_split_on_dot(alt, &number, &fraction); + if (ret != 0) { + goto error; + } + gnss_data.alt = number * 1000 + fraction; + } else { + gnss_data.alt = 0; + } + + ret = gnss_split_on_dot(hdop, &number, &fraction); + if (ret != 0) { + goto error; + } + gnss_data.hdop = number * 100 + fraction * 10; + + if (course) { + ret = gnss_split_on_dot(course, &number, &fraction); + if (ret != 0) { + goto error; + } + gnss_data.cog = number * 100 + fraction * 10; + } else { + gnss_data.cog = 0; + } + + if (speed) { + ret = gnss_split_on_dot(speed, &number, &fraction); + if (ret != 0) { + goto error; + } + gnss_data.kmh = number * 10 + fraction / 10; + } else { + gnss_data.kmh = 0; + } + + return 0; +error: + memset(&gnss_data, 0, sizeof(gnss_data)); + return -1; +} + +/* + * Parses the +CGNSINF Gnss response. + * + * The CGNSINF command has the following parameters but + * not all parameters are set by the module: + * + * +CGNSINF: ,,, + * ,,,, + * ,,,,, + * ,,,, + * , + * + */ +MODEM_CMD_DEFINE(on_cmd_cgnsinf) +{ + char gps_buf[MDM_GNSS_PARSER_MAX_LEN]; + size_t out_len = net_buf_linearize(gps_buf, sizeof(gps_buf) - 1, data->rx_buf, 0, len); + + gps_buf[out_len] = '\0'; + return parse_cgnsinf(gps_buf); +} + +int mdm_sim7080_query_gnss(struct sim7080_gnss_data *data) +{ + int ret; + struct modem_cmd cmds[] = { MODEM_CMD("+CGNSINF: ", on_cmd_cgnsinf, 0U, NULL) }; + + if (sim7080_get_state() != SIM7080_STATE_GNSS) { + LOG_ERR("GNSS functionality is not enabled!!"); + return -1; + } + + memset(&gnss_data, 0, sizeof(gnss_data)); + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CGNSINF", + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + return ret; + } + + if (!gnss_data.run_status || !gnss_data.fix_status) { + return -EAGAIN; + } + + if (data) { + memcpy(data, &gnss_data, sizeof(gnss_data)); + } + + return ret; +} + +static uint8_t cgnscpy_ret; + +MODEM_CMD_DEFINE(on_cmd_cgnscpy) +{ + cgnscpy_ret = (uint8_t)strtoul(argv[0], NULL, 10); + LOG_INF("CGNSCPY: %u", cgnscpy_ret); + return 0; +} + +static int16_t xtra_diff_h, xtra_duration_h; +static struct tm *xtra_inject; + +MODEM_CMD_DEFINE(on_cmd_cgnsxtra) +{ + xtra_diff_h = (int16_t)strtol(argv[0], NULL, 10); + xtra_duration_h = (int16_t)strtol(argv[1], NULL, 10); + int ret = sim7080_utils_parse_time(argv[2], argv[3], xtra_inject); + + LOG_INF("XTRA validity: diff=%d, duration=%d, inject=%s,%s", + xtra_diff_h, + xtra_duration_h, + argv[2], + argv[3]); + return ret; +} + +int mdm_sim7080_query_xtra_validity(int16_t *diff_h, int16_t *duration_h, struct tm *inject) +{ + struct modem_cmd cmds[] = { MODEM_CMD("+CGNSXTRA: ", on_cmd_cgnsxtra, 4U, ",") }; + int ret = -EINVAL; + + if (!diff_h || !duration_h || !inject) { + goto out; + } + + xtra_inject = inject; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CGNSXTRA", + &mdata.sem_response, K_SECONDS(2)); + if (ret != 0) { + LOG_ERR("Failed to query xtra validity"); + goto out; + } + + *diff_h = xtra_diff_h; + *duration_h = xtra_duration_h; + +out: + xtra_inject = NULL; + return ret; +} + +static int sim7080_start_gnss_ext(bool xtra) +{ + int ret = -EALREADY; + + if (sim7080_get_state() == SIM7080_STATE_GNSS) { + LOG_WRN("Modem already in gnss state"); + goto out; + } else if (sim7080_get_state() != SIM7080_STATE_IDLE) { + LOG_WRN("Can only activate gnss from idle state"); + ret = -EINVAL; + goto out; + } + + /* Power GNSS unit */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+CGNSPWR=1", + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("Failed to power on gnss: %d", ret); + goto out; + } + + if (xtra == false) { + goto coldstart; + } + + struct modem_cmd cmds[] = { MODEM_CMD("+CGNSCPY: ", on_cmd_cgnscpy, 1U, "") }; + + cgnscpy_ret = UINT8_MAX; + + /* Copy the xtra file to gnss unit */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CGNSCPY", + &mdata.sem_response, K_SECONDS(5)); + if (ret < 0) { + LOG_WRN("Failed to copy xtra file. Performing cold start"); + goto coldstart; + } + + if (cgnscpy_ret != 0) { + LOG_WRN("CGNSCPY returned %u. Performing cold start", cgnscpy_ret); + goto coldstart; + } + + /* Query the xtra file validity */ + int16_t diff, duration; + struct tm inject; + + ret = mdm_sim7080_query_xtra_validity(&diff, &duration, &inject); + if (ret != 0) { + LOG_WRN("Could not query xtra validity. Performing cold start"); + goto coldstart; + } + + if (diff < 0) { + LOG_WRN("XTRA file is not valid. Performing cold start"); + goto coldstart; + } + + /* Enable xtra functionality */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+CGNSXTRA=1", + &mdata.sem_response, K_SECONDS(5)); + if (ret < 0) { + LOG_WRN("Failed to enable xtra. Performing cold start"); + goto coldstart; + } + +coldstart: + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+CGNSCOLD", + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("Failed to start gnss: %d", ret); + goto out; + } + + sim7080_change_state(SIM7080_STATE_GNSS); +out: + return ret; +} + +int mdm_sim7080_start_gnss(void) +{ + return sim7080_start_gnss_ext(false); +} + +int mdm_sim7080_start_gnss_xtra(void) +{ + return sim7080_start_gnss_ext(true); +} + +int mdm_sim7080_stop_gnss(void) +{ + int ret = -EINVAL; + + if (sim7080_get_state() != SIM7080_STATE_GNSS) { + LOG_WRN("Modem not in gnss state"); + goto out; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, "AT+CGNSPWR=0", + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("Failed to power on gnss: %d", ret); + goto out; + } + + sim7080_change_state(SIM7080_STATE_IDLE); +out: + return ret; +} + +int mdm_sim7080_download_xtra(uint8_t server_id, const char *f_name) +{ + char buf[sizeof("AT+HTTPTOFS=\"http://iot#.xtracloud.net/xtra3##_72h.bin\",\"/customer/Xtra3.bin\"")]; + int ret = -ENOTCONN; + + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_WRN("Need network to download xtra file"); + goto out; + } + + ret = snprintk(buf, sizeof(buf), "AT+HTTPTOFS=\"http://iot%hhu.xtracloud.net/%s\",\"/customer/Xtra3.bin\"", + server_id, f_name); + if (ret < 0) { + LOG_ERR("Failed to format xtra download"); + goto out; + } + + mdata.http_status = 0; + + /* Download xtra file */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buf, + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("Failed to download xtra file"); + goto out; + } + + /* Wait for HTTP status code */ + ret = k_sem_take(&mdata.sem_http, K_SECONDS(60)); + if (ret != 0) { + LOG_ERR("Waiting for http completion failed"); + goto out; + } + + if (mdata.http_status != 200) { + LOG_ERR("HTTP request failed with: %u", mdata.http_status); + ret = -1; + } + +out: + return ret; +} diff --git a/drivers/modem/simcom/sim7080/sim7080_meas.c b/drivers/modem/simcom/sim7080/sim7080_meas.c new file mode 100644 index 0000000000000..3ed88c33eebd6 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_meas.c @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2025 Metratec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +LOG_MODULE_REGISTER(modem_sim7080_meas, CONFIG_MODEM_LOG_LEVEL); + +#include "sim7080.h" + +/* Common CPSI indices */ +#define CPSI_SYS_MODE_IDX 0U +#define CPSI_OP_MODE_IDX 1U +#define CPSI_MCC_MNC_IDX 2U +/* GSM specific CPSI indices */ +#define CPSI_GSM_LAC_IDX 3U +#define CPSI_GSM_CID_IDX 4U +#define CPSI_GSM_ARFCN_IDX 5U +#define CPSI_GSM_RX_LVL_IDX 6U +#define CPSI_GSM_TLO_ADJ_IDX 7U +#define CPSI_GSM_C1_C2_IDX 8U +/* LTE specific CPSI indices */ +#define CPSI_LTE_TAC_IDX 3U +#define CPSI_LTE_SCI_IDX 4U +#define CPSI_LTE_PCI_IDX 5U +#define CPSI_LTE_BAND_IDX 6U +#define CPSI_LTE_EARFCN_IDX 7U +#define CPSI_LTE_DLBW_IDX 8U +#define CPSI_LTE_ULBW_IDX 9U +#define CPSI_LTE_RSRQ_IDX 10U +#define CPSI_LTE_RSRP_IDX 11U +#define CPSI_LTE_RSSI_IDX 12U +#define CPSI_LTE_RSSNR_IDX 13U + +#define CPSI_GSM_ARG_COUNT 9U +#define CPSI_LTE_ARG_COUNT 14U + +static struct { + uint8_t bcs; + uint8_t bcl; + uint16_t volt; +} cbc_data; + +MODEM_CMD_DEFINE(on_cmd_cbc) +{ + long tmp; + + tmp = strtol(argv[0], NULL, 10); + if (errno == -ERANGE) { + return -EBADMSG; + } + cbc_data.bcs = (uint8_t)tmp; + tmp = strtol(argv[1], NULL, 10); + if (errno == -ERANGE) { + return -EBADMSG; + } + cbc_data.bcl = (uint8_t)tmp; + tmp = strtol(argv[2], NULL, 10); + if (errno == -ERANGE) { + return -EBADMSG; + } + cbc_data.volt = (uint16_t)tmp; + + return 0; +} + +int mdm_sim7080_get_battery_charge(uint8_t *bcs, uint8_t *bcl, uint16_t *voltage) +{ + int ret; + struct modem_cmd cmds[] = {MODEM_CMD("+CBC: ", on_cmd_cbc, 3U, ",")}; + + if (sim7080_get_state() == SIM7080_STATE_OFF) { + LOG_ERR("SIM7080 not powered on!"); + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CBC", + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + return ret; + } + + *bcs = cbc_data.bcs; + *bcl = cbc_data.bcl; + *voltage = cbc_data.volt; + + return ret; +} + +static const uint8_t *ue_sys_mode_lut[] = { + "NO SERVICE", + "GSM", + "LTE CAT-M1", + "LTE NB-IOT", +}; + +static const uint8_t *ue_op_mode_lut[] = { + "Online", + "Offline", + "Factory Test Mode", + "Reset", + "Low Power Mode", +}; + +/** + * Check lookup table for match. + * @param s 0 terminated source string. + * @param lut The lookup table. + * @param size Size of the lookup table. + * @return Index in the lookup table or -1 on no match. + */ +static int8_t lut_match(const uint8_t *s, const uint8_t **lut, uint8_t size) +{ + for (uint8_t i = 0; i < size; i++) { + if (strcmp(s, lut[i]) == 0) { + return i; + } + } + + return -1; +} + +static int cpsi_parse_minus(uint8_t *s, uint16_t *a, uint16_t *b) +{ + char *saveptr; + char *tmp = strtok_r(s, "-", &saveptr); + + if (tmp == NULL) { + return -1; + } + + *a = (uint16_t)strtoul(tmp, NULL, 10); + + tmp = strtok_r(NULL, NULL, &saveptr); + if (tmp == NULL) { + return -1; + } + + *b = (uint16_t)strtoul(tmp, NULL, 10); + + return 0; +} + +static int cpsi_parse_gsm(struct sim7080_ue_sys_info *info, uint8_t **argv, uint16_t argc) +{ + int ret = -EINVAL; + + if (argc != CPSI_GSM_ARG_COUNT) { + LOG_ERR("Unexpected number of arguments: %u", argc); + goto out; + } + + ret = cpsi_parse_minus(argv[CPSI_MCC_MNC_IDX], &info->cell.gsm.mcc, &info->cell.gsm.mcn); + if (ret != 0) { + LOG_ERR("Failed to parse MCC/MCN"); + goto out; + } + + info->cell.gsm.lac = (uint16_t)strtoul(argv[CPSI_GSM_LAC_IDX], NULL, 16); + info->cell.gsm.cid = (uint16_t)strtoul(argv[CPSI_GSM_CID_IDX], NULL, 10); + strncpy(info->cell.gsm.arfcn, argv[CPSI_GSM_ARFCN_IDX], sizeof(info->cell.gsm.arfcn) - 1); + info->cell.gsm.rx_lvl = (int16_t)strtol(argv[CPSI_GSM_CID_IDX], NULL, 10); + info->cell.gsm.track_lo_adjust = (int16_t)strtol(argv[CPSI_GSM_TLO_ADJ_IDX], NULL, 10); + + ret = cpsi_parse_minus(argv[CPSI_GSM_C1_C2_IDX], &info->cell.gsm.c1, &info->cell.gsm.c2); + if (ret != 0) { + LOG_ERR("Failed to parse C1/C2"); + goto out; + } + +out: + return ret; +} + +static int cpsi_parse_lte(struct sim7080_ue_sys_info *info, uint8_t **argv, uint16_t argc) +{ + int ret = -EINVAL; + + if (argc != CPSI_LTE_ARG_COUNT) { + LOG_ERR("Unexpected number of arguments: %u", argc); + goto out; + } + + ret = cpsi_parse_minus(argv[CPSI_MCC_MNC_IDX], &info->cell.lte.mcc, &info->cell.lte.mcn); + if (ret != 0) { + LOG_ERR("Failed to parse MCC/MCN"); + goto out; + } + + info->cell.lte.tac = (uint16_t)strtoul(argv[CPSI_LTE_TAC_IDX], NULL, 16); + info->cell.lte.sci = (uint32_t)strtoul(argv[CPSI_LTE_SCI_IDX], NULL, 10); + info->cell.lte.pci = (uint16_t)strtoul(argv[CPSI_LTE_PCI_IDX], NULL, 10); + strncpy(info->cell.lte.band, argv[CPSI_LTE_BAND_IDX], sizeof(info->cell.lte.band) - 1); + info->cell.lte.earfcn = (uint16_t)strtoul(argv[CPSI_LTE_EARFCN_IDX], NULL, 10); + info->cell.lte.dlbw = (uint16_t)strtoul(argv[CPSI_LTE_DLBW_IDX], NULL, 10); + info->cell.lte.ulbw = (uint16_t)strtoul(argv[CPSI_LTE_ULBW_IDX], NULL, 10); + info->cell.lte.rsrq = (int16_t)strtol(argv[CPSI_LTE_RSRQ_IDX], NULL, 10); + info->cell.lte.rsrp = (int16_t)strtol(argv[CPSI_LTE_RSRP_IDX], NULL, 10); + info->cell.lte.rssi = (int16_t)strtol(argv[CPSI_LTE_RSSI_IDX], NULL, 10); + info->cell.lte.rssnr = (int16_t)strtol(argv[CPSI_LTE_RSSNR_IDX], NULL, 10); + info->cell.lte.sinr = 2 * info->cell.lte.rssnr - 20; + +out: + return ret; +} + +static struct sim7080_ue_sys_info *ue_sys_info; + +MODEM_CMD_DEFINE(on_cmd_cpsi) +{ + int ret = -1; + + memset(ue_sys_info, 0, sizeof(*ue_sys_info)); + + if (argc < 2) { + LOG_ERR("Insufficient number of parameters: %u", argc); + goto out; + } + + ret = lut_match(argv[CPSI_SYS_MODE_IDX], ue_sys_mode_lut, ARRAY_SIZE(ue_sys_mode_lut)); + if (ret < 0) { + LOG_ERR("Illegal sys mode: %s", argv[CPSI_SYS_MODE_IDX]); + goto out; + } + + ue_sys_info->sys_mode = (enum sim7080_ue_sys_mode)ret; + + ret = lut_match(argv[CPSI_OP_MODE_IDX], ue_op_mode_lut, ARRAY_SIZE(ue_op_mode_lut)); + if (ret < 0) { + LOG_ERR("Illegal op mode: %s", argv[CPSI_OP_MODE_IDX]); + goto out; + } + + ue_sys_info->op_mode = (enum sim7080_ue_op_mode)ret; + + if (ue_sys_info->sys_mode == SIM7080_UE_SYS_MODE_NO_SERVICE) { + /* No further information available */ + ret = 0; + } else if (ue_sys_info->sys_mode == SIM7080_UE_SYS_MODE_GSM) { + ret = cpsi_parse_gsm(ue_sys_info, argv, argc); + } else if (ue_sys_info->sys_mode == SIM7080_UE_SYS_MODE_LTE_CAT_M1 || + ue_sys_info->sys_mode == SIM7080_UE_SYS_MODE_LTE_NB_IOT) { + ret = cpsi_parse_lte(ue_sys_info, argv, argc); + } + +out: + return ret; +} + +int mdm_sim7080_get_ue_sys_info(struct sim7080_ue_sys_info *info) +{ + int ret = -1; + struct modem_cmd cmds[] = {MODEM_CMD("+CPSI: ", on_cmd_cpsi, 14U, ",")}; + + if (sim7080_get_state() == SIM7080_STATE_OFF) { + LOG_ERR("SIM7080 not powered on!"); + goto out; + } + + if (info == NULL) { + ret = -EINVAL; + goto out; + } + + ue_sys_info = info; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CPSI?", + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + goto out; + } + +out: + return ret; +} + +static struct tm *local_tm; + +MODEM_CMD_DEFINE(on_cmd_cclk) +{ + char *saveptr; + int ret = -1; + + /* +1 to skip leading " */ + char *date = strtok_r(argv[0] + 1, ",", &saveptr); + + if (date == NULL) { + LOG_WRN("Failed to parse date"); + goto out; + } + + char *time_str = strtok_r(NULL, "\"", &saveptr); + + if (time_str == NULL) { + LOG_WRN("Failed to parse time"); + goto out; + } + + ret = sim7080_utils_parse_time(date, time_str, local_tm); + +out: + return ret; +} + +int mdm_sim7080_get_local_time(struct tm *t) +{ + int ret = -1; + struct modem_cmd cmds[] = {MODEM_CMD("+CCLK: ", on_cmd_cclk, 1U, ",")}; + + if (sim7080_get_state() == SIM7080_STATE_OFF) { + goto out; + } + + if (!t) { + ret = -EINVAL; + goto out; + } + + local_tm = t; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CCLK?", + &mdata.sem_response, K_SECONDS(2)); + +out: + local_tm = NULL; + return ret; +} diff --git a/drivers/modem/simcom/sim7080/sim7080_pdp.c b/drivers/modem/simcom/sim7080/sim7080_pdp.c new file mode 100644 index 0000000000000..72c43eab53d6a --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_pdp.c @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2025 metraTec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +LOG_MODULE_REGISTER(modem_simcom_sim7080_pdp, CONFIG_MODEM_LOG_LEVEL); + +#include "sim7080.h" + +static const struct setup_cmd band_setup_cmds[] = { + #if defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_NB1) + SETUP_CMD_NOHANDLE("AT+CNMP=38"), + SETUP_CMD_NOHANDLE("AT+CMNB=2"), + SETUP_CMD_NOHANDLE("AT+CBANDCFG=\"NB-IOT\"," MDM_LTE_BANDS), + #endif /* defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_NB1) */ + #if defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_M1) + SETUP_CMD_NOHANDLE("AT+CNMP=38"), + SETUP_CMD_NOHANDLE("AT+CMNB=1"), + SETUP_CMD_NOHANDLE("AT+CBANDCFG=\"CAT-M\"," MDM_LTE_BANDS), + #endif /* defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_M1) */ + #if defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_GSM) + SETUP_CMD_NOHANDLE("AT+CNMP=13"), + #endif /* defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_GSM) */ +}; + +/* + * Handler for RSSI query. + * + * +CSQ: , + * rssi: 0,-115dBm; 1,-111dBm; 2...30,-110...-54dBm; 31,-52dBm or greater. + * 99, ukn + * ber: Not used. + */ +MODEM_CMD_DEFINE(on_cmd_csq) +{ + int rssi = atoi(argv[0]); + + if (rssi == 0) { + mdata.mdm_rssi = -115; + } else if (rssi == 1) { + mdata.mdm_rssi = -111; + } else if (rssi > 1 && rssi < 31) { + mdata.mdm_rssi = -114 + 2 * rssi; + } else if (rssi == 31) { + mdata.mdm_rssi = -52; + } else { + mdata.mdm_rssi = -1000; + } + + LOG_INF("RSSI: %d", mdata.mdm_rssi); + return 0; +} + +/* + * Queries modem RSSI. + * + * If a work queue parameter is provided query work will + * be scheduled. Otherwise rssi is queried once. + */ +void sim7080_rssi_query_work(struct k_work *work) +{ + struct modem_cmd cmd[] = { MODEM_CMD("+CSQ: ", on_cmd_csq, 2U, ",") }; + static char *send_cmd = "AT+CSQ"; + int ret; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmd, ARRAY_SIZE(cmd), send_cmd, + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("AT+CSQ ret:%d", ret); + } + + if (work) { + k_work_reschedule_for_queue(&modem_workq, &mdata.rssi_query_work, + K_SECONDS(RSSI_TIMEOUT_SECS)); + } +} + +/* + * Parses the non urc C(E)REG and updates registration status. + */ +MODEM_CMD_DEFINE(on_cmd_cereg) +{ + mdata.mdm_registration = atoi(argv[1]); + LOG_INF("CREG: %u", mdata.mdm_registration); + return 0; +} + +MODEM_CMD_DEFINE(on_cmd_cgatt) +{ + int cgatt = atoi(argv[0]); + + if (cgatt) { + mdata.status_flags |= SIM7080_STATUS_FLAG_ATTACHED; + } else { + mdata.status_flags &= ~SIM7080_STATUS_FLAG_ATTACHED; + } + + LOG_INF("CGATT: %d", cgatt); + return 0; +} + +int sim7080_pdp_activate(void) +{ + int counter; + int ret = 0; +#if defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_GSM) + const char *buf = "AT+CREG?"; + struct modem_cmd cmds[] = { MODEM_CMD("+CREG: ", on_cmd_cereg, 2U, ",") }; +#else + const char *buf = "AT+CEREG?"; + struct modem_cmd cmds[] = { MODEM_CMD("+CEREG: ", on_cmd_cereg, 2U, ",") }; +#endif /* defined(CONFIG_MODEM_SIMCOM_SIM7080_RAT_GSM) */ + + /* Set the preferred bands */ + ret = modem_cmd_handler_setup_cmds(&mctx.iface, &mctx.cmd_handler, band_setup_cmds, + ARRAY_SIZE(band_setup_cmds), &mdata.sem_response, + MDM_REGISTRATION_TIMEOUT); + if (ret != 0) { + LOG_ERR("Failed to send band setup commands"); + goto error; + } + + /* Wait for acceptable rssi values. */ + sim7080_rssi_query_work(NULL); + + counter = 0; + while (counter++ < MDM_WAIT_FOR_RSSI_COUNT && + (mdata.mdm_rssi >= 0 || mdata.mdm_rssi <= -1000)) { + k_sleep(MDM_WAIT_FOR_RSSI_DELAY); + sim7080_rssi_query_work(NULL); + } + + if (mdata.mdm_rssi >= 0 || mdata.mdm_rssi <= -1000) { + LOG_ERR("No valid RSSI reached"); + ret = -ENETUNREACH; + goto error; + } + + struct modem_cmd cgatt_cmd[] = { MODEM_CMD("+CGATT: ", on_cmd_cgatt, 1U, "") }; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cgatt_cmd, + ARRAY_SIZE(cgatt_cmd), "AT+CGATT?", &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to query cgatt"); + goto error; + } + + counter = 0; + while (counter++ < MDM_MAX_CGATT_WAITS && + (mdata.status_flags & SIM7080_STATUS_FLAG_ATTACHED) == 0) { + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cgatt_cmd, + ARRAY_SIZE(cgatt_cmd), "AT+CGATT?", &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to query cgatt"); + goto error; + } + + k_sleep(K_SECONDS(1)); + } + + if ((mdata.status_flags & SIM7080_STATUS_FLAG_CPIN_READY) == 0 || + (mdata.status_flags & SIM7080_STATUS_FLAG_ATTACHED) == 0) { + LOG_ERR("Fatal: Modem is not attached to GPRS network"); + ret = -ENETUNREACH; + goto error; + } + + LOG_INF("Waiting for network"); + + /* Wait until the module is registered to the network. + * Registration will be set by urc. + */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), buf, + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to query registration"); + goto error; + } + + counter = 0; + while (counter++ < MDM_MAX_CEREG_WAITS && mdata.mdm_registration != 1 && + mdata.mdm_registration != 5) { + k_sleep(K_SECONDS(1)); + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), buf, + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to query registration"); + goto error; + } + } + + if (mdata.mdm_registration != 1 && mdata.mdm_registration != 5) { + LOG_WRN("Network registration failed!"); + ret = -ENETUNREACH; + goto error; + } + + /* Set dual stack mode (IPv4/IPv6) */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0, "AT+CNCFG=0,0", + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Could not configure pdp context!"); + goto error; + } + + /* + * Now activate the pdp context and wait for confirmation. + */ + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0, "AT+CNACT=0,1", + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Could not activate PDP context."); + goto error; + } + + k_sem_reset(&mdata.pdp_sem); + ret = k_sem_take(&mdata.pdp_sem, MDM_PDP_TIMEOUT); + if (ret < 0 || (mdata.status_flags & SIM7080_STATUS_FLAG_PDP_ACTIVE) == 0) { + LOG_ERR("Failed to activate PDP context."); + ret = -ENETUNREACH; + goto error; + } + + LOG_INF("Network active."); + sim7080_change_state(SIM7080_STATE_NETWORKING); + + k_work_reschedule_for_queue(&modem_workq, &mdata.rssi_query_work, + K_SECONDS(RSSI_TIMEOUT_SECS)); + +error: + return ret; +} + +int sim7080_pdp_deactivate(void) +{ + int ret = -EINVAL; + + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_WRN("Cannot deactivate pdp context in state: %d", sim7080_get_state()); + goto out; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0, "AT+CNACT=0,0", + &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Could not deactivate PDP context."); + goto out; + } + + k_sem_reset(&mdata.pdp_sem); + ret = k_sem_take(&mdata.pdp_sem, MDM_PDP_TIMEOUT); + if (ret < 0 || (mdata.status_flags & SIM7080_STATUS_FLAG_PDP_ACTIVE) != 0) { + LOG_ERR("PDP response timed out"); + ret = -EIO; + } + + k_work_cancel_delayable(&mdata.rssi_query_work); + + LOG_INF("PDP context deactivated"); + sim7080_change_state(SIM7080_STATE_IDLE); + +out: + return ret; +} diff --git a/drivers/modem/simcom/sim7080/sim7080_sms.c b/drivers/modem/simcom/sim7080/sim7080_sms.c new file mode 100644 index 0000000000000..118149f3561e5 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_sms.c @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2025 Metratec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +LOG_MODULE_REGISTER(modem_sim7080_sms, CONFIG_MODEM_LOG_LEVEL); + +#include "sim7080.h" + +#define SMS_TP_UDHI_HEADER 0x40 + +/** + * Decode readable hex to "real" hex. + */ +static uint8_t mdm_pdu_decode_ascii(char byte) +{ + if ((byte >= '0') && (byte <= '9')) { + return byte - '0'; + } else if ((byte >= 'A') && (byte <= 'F')) { + return byte - 'A' + 10; + } else if ((byte >= 'a') && (byte <= 'f')) { + return byte - 'a' + 10; + } else { + return 255; + } +} + +/** + * Reads "byte" from pdu. + * + * @param pdu pdu to read from. + * @param index index of "byte". + * + * Sim module "encodes" one pdu byte as two human readable bytes + * this functions squashes these two bytes into one. + */ +static uint8_t mdm_pdu_read_byte(const char *pdu, size_t index) +{ + return (mdm_pdu_decode_ascii(pdu[index * 2]) << 4 | + mdm_pdu_decode_ascii(pdu[index * 2 + 1])); +} + +/** + * Decodes time from pdu. + * + * @param pdu pdu to read from. + * @param index index of "byte". + */ +static uint8_t mdm_pdu_read_time(const char *pdu, size_t index) +{ + return (mdm_pdu_decode_ascii(pdu[index * 2]) + + mdm_pdu_decode_ascii(pdu[index * 2 + 1]) * 10); +} + +/** + * Decode a sms from pdu mode. + */ +static int mdm_decode_pdu(const char *pdu, size_t pdu_len, struct sim7080_sms *target_buf) +{ + size_t index; + + /* + * GSM_03.38 to Unicode conversion table + */ + const short enc7_basic[128] = { + '@', 0xA3, '$', 0xA5, 0xE8, 0xE9, 0xF9, 0xEC, 0xF2, 0xE7, + '\n', 0xD8, 0xF8, '\r', 0xC5, 0xF8, 0x0394, '_', 0x03A6, 0x0393, + 0x039B, 0x03A9, 0x03A0, 0x03A8, 0x03A3, 0x0398, 0x039E, '\x1b', 0xC6, 0xE6, + 0xDF, 0xC9, ' ', '!', '\"', '#', 0xA4, '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', + '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', + '<', '=', '>', '?', 0xA1, 'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 0xC4, 0xD6, 0xD1, 0xDC, 0xA7, 0xBF, 'a', 'b', 'c', + 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', + 'x', 'y', 'z', 0xE4, 0xF6, 0xF1, 0xFC, 0xE0 + }; + + /* two bytes in pdu are on real byte */ + pdu_len = (pdu_len / 2); + + /* first byte of pdu is length of trailing SMSC information + * skip it by setting index to SMSC length + 1. + */ + index = mdm_pdu_read_byte(pdu, 0) + 1; + + if (index >= pdu_len) { + return -1; + } + + /* read first octet */ + target_buf->first_octet = mdm_pdu_read_byte(pdu, index++); + + if (index >= pdu_len) { + return -1; + } + + /* pdu_index now points to the address field. + * first byte of addr field is the addr length -> skip it. + * address type is not included in addr len -> add +1. + * address is coded in semi octets + * + addr_len/2 if even + * + addr_len/2 + 1 if odd + */ + uint8_t addr_len = mdm_pdu_read_byte(pdu, index); + + index += ((addr_len % 2) == 0) ? (addr_len / 2) + 2 : (addr_len / 2) + 3; + + if (index >= pdu_len) { + return -1; + } + + /* read protocol identifier */ + target_buf->tp_pid = mdm_pdu_read_byte(pdu, index++); + + if (index >= pdu_len) { + return -1; + } + + /* read coding scheme */ + uint8_t tp_dcs = mdm_pdu_read_byte(pdu, index++); + + /* parse date and time */ + if ((index + 7) >= pdu_len) { + return -1; + } + + target_buf->time.year = mdm_pdu_read_time(pdu, index++); + target_buf->time.month = mdm_pdu_read_time(pdu, index++); + target_buf->time.day = mdm_pdu_read_time(pdu, index++); + target_buf->time.hour = mdm_pdu_read_time(pdu, index++); + target_buf->time.minute = mdm_pdu_read_time(pdu, index++); + target_buf->time.second = mdm_pdu_read_time(pdu, index++); + target_buf->time.timezone = mdm_pdu_read_time(pdu, index++); + + /* Read user data length */ + uint8_t tp_udl = mdm_pdu_read_byte(pdu, index++); + + /* Discard header */ + uint8_t header_skip = 0; + + if (target_buf->first_octet & SMS_TP_UDHI_HEADER) { + uint8_t tp_udhl = mdm_pdu_read_byte(pdu, index); + + index += tp_udhl + 1; + header_skip = tp_udhl + 1; + + if (index >= pdu_len) { + return -1; + } + } + + /* Read data according to type set in TP-DCS */ + if (tp_dcs == 0x00) { + /* 7 bit GSM coding */ + uint8_t fill_level = 0; + uint16_t buf = 0; + + if (target_buf->first_octet & SMS_TP_UDHI_HEADER) { + /* Initial fill because septets are aligned to + * septet boundary after header + */ + uint8_t fill_bits = 7 - ((header_skip * 8) % 7); + + if (fill_bits == 7) { + fill_bits = 0; + } + + buf = mdm_pdu_read_byte(pdu, index++); + + fill_level = 8 - fill_bits; + } + + uint16_t data_index = 0; + + for (unsigned int idx = 0; idx < tp_udl; idx++) { + if (fill_level < 7) { + uint8_t octet = mdm_pdu_read_byte(pdu, index++); + + buf &= ((1 << fill_level) - 1); + buf |= (octet << fill_level); + fill_level += 8; + } + + /* + * Convert 7-bit encoded data to Unicode and + * then to UTF-8 + */ + short letter = enc7_basic[buf & 0x007f]; + + if (letter < 0x0080) { + target_buf->data[data_index++] = letter & 0x007f; + } else if (letter < 0x0800) { + target_buf->data[data_index++] = 0xc0 | ((letter & 0x07c0) >> 6); + target_buf->data[data_index++] = 0x80 | ((letter & 0x003f) >> 0); + } + buf >>= 7; + fill_level -= 7; + } + target_buf->data_len = data_index; + } else if (tp_dcs == 0x04) { + /* 8 bit binary coding */ + for (int idx = 0; idx < tp_udl - header_skip; idx++) { + target_buf->data[idx] = mdm_pdu_read_byte(pdu, index++); + } + target_buf->data_len = tp_udl; + } else if (tp_dcs == 0x08) { + /* Unicode (16 bit per character) */ + for (int idx = 0; idx < tp_udl - header_skip; idx++) { + target_buf->data[idx] = mdm_pdu_read_byte(pdu, index++); + } + target_buf->data_len = tp_udl; + } else { + return -1; + } + + return 0; +} + +/** + * Check if given char sequence is crlf. + * + * @param c The char sequence. + * @param len Total length of the fragment. + * @return @c true if char sequence is crlf. + * Otherwise @c false is returned. + */ +static bool is_crlf(uint8_t *c, uint8_t len) +{ + /* crlf does not fit. */ + if (len < 2) { + return false; + } + + return c[0] == '\r' && c[1] == '\n'; +} + +/** + * Find terminating crlf in a netbuffer. + * + * @param buf The netbuffer. + * @param skip Bytes to skip before search. + * @return Length of the returned fragment or 0 if not found. + */ +static size_t net_buf_find_crlf(struct net_buf *buf, size_t skip) +{ + size_t len = 0, pos = 0; + struct net_buf *frag = buf; + + /* Skip to the start. */ + while (frag && skip >= frag->len) { + skip -= frag->len; + frag = frag->frags; + } + + /* Need to wait for more data. */ + if (!frag) { + return 0; + } + + pos = skip; + + while (frag && !is_crlf(frag->data + pos, frag->len - pos)) { + if (pos + 1 >= frag->len) { + len += frag->len; + frag = frag->frags; + pos = 0U; + } else { + pos++; + } + } + + if (frag && is_crlf(frag->data + pos, frag->len - pos)) { + len += pos; + return len - skip; + } + + return 0; +} + +/** + * Parses list sms and add them to buffer. + * Format is: + * + * +CMGL: ,,, + * +CMGL: ,,, + * ... + * OK + */ +MODEM_CMD_DEFINE(on_cmd_cmgl) +{ + int sms_index, sms_stat, ret; + char pdu_buffer[256]; + size_t out_len, sms_len, param_len; + struct sim7080_sms *sms; + + sms_index = atoi(argv[0]); + sms_stat = atoi(argv[1]); + + /* Get the length of the "length" parameter. + * The last parameter will be stuck in the netbuffer. + * It is not the actual length of the trailing pdu so + * we have to search the next crlf. + */ + param_len = net_buf_find_crlf(data->rx_buf, 0); + if (param_len == 0) { + LOG_INF("No "); + return -EAGAIN; + } + + /* Get actual trailing pdu len. +2 to skip crlf. */ + sms_len = net_buf_find_crlf(data->rx_buf, param_len + 2); + if (sms_len == 0) { + return -EAGAIN; + } + + /* Skip to start of pdu. */ + data->rx_buf = net_buf_skip(data->rx_buf, param_len + 2); + + out_len = net_buf_linearize(pdu_buffer, sizeof(pdu_buffer) - 1, data->rx_buf, 0, sms_len); + pdu_buffer[out_len] = '\0'; + + data->rx_buf = net_buf_skip(data->rx_buf, sms_len); + + /* No buffer specified. */ + if (!mdata.sms_buffer) { + return 0; + } + + /* No space left in buffer. */ + if (mdata.sms_buffer_pos >= mdata.sms_buffer->nsms) { + return 0; + } + + sms = &mdata.sms_buffer->sms[mdata.sms_buffer_pos]; + + ret = mdm_decode_pdu(pdu_buffer, out_len, sms); + if (ret < 0) { + return 0; + } + + sms->stat = sms_stat; + sms->index = sms_index; + sms->data[sms->data_len] = '\0'; + + mdata.sms_buffer_pos++; + + return 0; +} + +int mdm_sim7080_read_sms(struct sim7080_sms_buffer *buffer) +{ + int ret; + struct modem_cmd cmds[] = { MODEM_CMD("+CMGL: ", on_cmd_cmgl, 4U, ",\r") }; + + mdata.sms_buffer = buffer; + mdata.sms_buffer_pos = 0; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmds, ARRAY_SIZE(cmds), "AT+CMGL=4", + &mdata.sem_response, K_SECONDS(20)); + if (ret < 0) { + return -1; + } + + return mdata.sms_buffer_pos; +} + +int mdm_sim7080_delete_sms(uint16_t index) +{ + int ret; + char buf[sizeof("AT+CMGD=#####")] = { 0 }; + + ret = snprintk(buf, sizeof(buf), "AT+CMGD=%u", index); + if (ret < 0) { + return -1; + } + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0, buf, &mdata.sem_response, + K_SECONDS(5)); + if (ret < 0) { + return -1; + } + + return 0; +} diff --git a/drivers/modem/simcom/sim7080/sim7080_sock.c b/drivers/modem/simcom/sim7080/sim7080_sock.c new file mode 100644 index 0000000000000..66b52d9a141a4 --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_sock.c @@ -0,0 +1,647 @@ +/* + * Copyright (C) 2025 metraTec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT simcom_sim7080 + +#include +#include +LOG_MODULE_REGISTER(modem_simcom_sim7080_sock, CONFIG_MODEM_LOG_LEVEL); + +#include +#include "sim7080.h" + +static void socket_close(struct modem_socket *sock); + +/* + * Parses the +CAOPEN command and gives back the + * connect semaphore. + */ +MODEM_CMD_DEFINE(on_cmd_caopen) +{ + int result = atoi(argv[1]); + + LOG_INF("+CAOPEN: %d", result); + mdata.socket_open_rc = result; + return 0; +} + +/* + * Connects an modem socket. Protocol can either be TCP or UDP. + */ +static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + uint16_t dst_port = 0; + char *protocol; + struct modem_cmd cmd[] = { MODEM_CMD("+CAOPEN: ", on_cmd_caopen, 2U, ",") }; + char buf[sizeof("AT+CAOPEN: #,#,#####," + "#xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxx.xxx.xxx.xxx#,####")]; + char ip_str[NET_IPV6_ADDR_LEN]; + int ret; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + return -EINVAL; + } + + if (modem_socket_is_allocated(&mdata.socket_config, sock) == false) { + LOG_ERR("Invalid socket id %d from fd %d", sock->id, sock->sock_fd); + errno = EINVAL; + return -1; + } + + if (sock->is_connected == true) { + LOG_ERR("Socket is already connected! id: %d, fd: %d", sock->id, sock->sock_fd); + errno = EISCONN; + return -1; + } + + /* get the destination port */ + if (addr->sa_family == AF_INET6) { + dst_port = ntohs(net_sin6(addr)->sin6_port); + } else if (addr->sa_family == AF_INET) { + dst_port = ntohs(net_sin(addr)->sin_port); + } + + /* Get protocol */ + protocol = (sock->type == SOCK_STREAM) ? "TCP" : "UDP"; + + ret = modem_context_sprint_ip_addr(addr, ip_str, sizeof(ip_str)); + if (ret != 0) { + LOG_ERR("Failed to format IP!"); + errno = ENOMEM; + return -1; + } + + ret = snprintk(buf, sizeof(buf), "AT+CAOPEN=%d,0,\"%s\",\"%s\",%d", sock->id, + protocol, ip_str, dst_port); + if (ret < 0) { + LOG_ERR("Failed to build connect command. ID: %d, FD: %d", sock->id, sock->sock_fd); + errno = ENOMEM; + return -1; + } + + mdata.socket_open_rc = 1; + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmd, ARRAY_SIZE(cmd), buf, + &mdata.sem_response, MDM_CONNECT_TIMEOUT); + if (ret < 0) { + LOG_ERR("%s ret: %d", buf, ret); + goto error; + } + + if (mdata.socket_open_rc != 0) { + LOG_ERR("Failed to open the socket: %u", mdata.socket_open_rc); + ret = -ENOTCONN; + goto error; + } + + sock->is_connected = true; + errno = 0; + return 0; +error: + errno = -ret; + return -1; +} + +MODEM_CMD_DEFINE(on_cmd_casend) +{ + mdata.tx_space_avail = strtoul(argv[0], NULL, 10); + LOG_DBG("Available tx space: %zu", mdata.tx_space_avail); + return 0; +} + +/* + * Send data over a given socket. + * + * First we signal the module that we want to send data over a socket. + * This is done by sending AT+CASEND=,\r\n. + * If The module is ready to send data it will send back + * an UNTERMINATED prompt '> '. After that data can be sent to the modem. + * As terminating byte a STRG+Z (0x1A) is sent. The module will + * then send a OK or ERROR. + */ +static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags, + const struct sockaddr *dest_addr, socklen_t addrlen) +{ + int ret; + struct modem_socket *sock = (struct modem_socket *)obj; + char send_buf[sizeof("AT+CASEND=#,####")] = { 0 }; + struct modem_cmd cmd[] = { MODEM_CMD("+CASEND: ", on_cmd_casend, 1U, "") }; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_ERR("Modem currently not attached to the network!"); + return -EINVAL; + } + + /* Do some sanity checks. */ + if (!buf || len == 0) { + errno = EINVAL; + return -1; + } + + /* Socket has to be connected. */ + if (!sock->is_connected) { + errno = ENOTCONN; + return -1; + } + + /* Query the available space in send buffer */ + ret = snprintk(send_buf, sizeof(send_buf), "AT+CASEND=%d", sock->id); + if (ret < 0) { + LOG_ERR("Failed to build send query command"); + errno = ENOMEM; + return -1; + } + + mdata.tx_space_avail = 0; + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, cmd, ARRAY_SIZE(cmd), send_buf, + &mdata.sem_response, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("Failed to query available tx size: %d", ret); + errno = EIO; + return -1; + } + + if (mdata.tx_space_avail == 0) { + LOG_WRN("No space left in tx buffer"); + errno = ENOMEM; + return -1; + } + + /* Only send up to available tx size bytes. */ + if (len > mdata.tx_space_avail) { + len = mdata.tx_space_avail; + } + + ret = snprintk(send_buf, sizeof(send_buf), "AT+CASEND=%d,%zu", sock->id, len); + if (ret < 0) { + LOG_ERR("Failed to build send command"); + errno = ENOMEM; + return -1; + } + + /* Make sure only one send can be done at a time. */ + k_sem_take(&mdata.cmd_handler_data.sem_tx_lock, K_FOREVER); + k_sem_reset(&mdata.sem_tx_ready); + + /* Send CASEND */ + mdata.current_sock_written = len; + ret = modem_cmd_send_nolock(&mctx.iface, &mctx.cmd_handler, NULL, 0U, send_buf, NULL, + K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to send CASEND"); + goto exit; + } + + /* Wait for '> ' */ + ret = k_sem_take(&mdata.sem_tx_ready, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("Timeout while waiting for tx"); + goto exit; + } + + /* Send data */ + modem_cmd_send_data_nolock(&mctx.iface, buf, len); + + /* Wait for the OK */ + k_sem_reset(&mdata.sem_response); + ret = k_sem_take(&mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("Timeout waiting for OK"); + } + +exit: + k_sem_give(&mdata.cmd_handler_data.sem_tx_lock); + /* Data was successfully sent */ + + if (ret < 0) { + errno = -ret; + return -1; + } + + errno = 0; + return mdata.current_sock_written; +} + +/* + * Read data from a given socket. + * + * The response has the form +CARECV: ,data\r\nOK\r\n + */ +static int sockread_common(int sockfd, struct modem_cmd_handler_data *data, int socket_data_length, + uint16_t len) +{ + struct modem_socket *sock; + struct socket_read_data *sock_data; + int ret, packet_size; + + if (!len) { + LOG_ERR("Invalid length, aborting"); + return -EAGAIN; + } + + if (!data->rx_buf) { + LOG_ERR("Incorrect format! Ignoring data!"); + return -EINVAL; + } + + if (socket_data_length <= 0) { + LOG_ERR("Length error (%d)", socket_data_length); + return -EAGAIN; + } + + if (net_buf_frags_len(data->rx_buf) < socket_data_length) { + LOG_DBG("Not enough data -- wait!"); + return -EAGAIN; + } + + sock = modem_socket_from_fd(&mdata.socket_config, sockfd); + if (!sock) { + LOG_ERR("Socket not found! (%d)", sockfd); + ret = -EINVAL; + goto exit; + } + + sock_data = (struct socket_read_data *)sock->data; + if (!sock_data) { + LOG_ERR("Socket data not found! (%d)", sockfd); + ret = -EINVAL; + goto exit; + } + + ret = net_buf_linearize(sock_data->recv_buf, sock_data->recv_buf_len, data->rx_buf, 0, + (uint16_t)socket_data_length); + data->rx_buf = net_buf_skip(data->rx_buf, ret); + sock_data->recv_read_len = ret; + if (ret != socket_data_length) { + LOG_ERR("Total copied data is different then received data!" + " copied:%d vs. received:%d", + ret, socket_data_length); + ret = -EINVAL; + goto exit; + } + +exit: + /* Indication only sets length to a dummy value. */ + packet_size = modem_socket_next_packet_size(&mdata.socket_config, sock); + modem_socket_packet_size_update(&mdata.socket_config, sock, -packet_size); + return ret; +} + +/* + * Handler for carecv response. + */ +MODEM_CMD_DEFINE(on_cmd_carecv) +{ + return sockread_common(mdata.current_sock_fd, data, atoi(argv[0]), len); +} + +/* + * Read data from a given socket. + */ +static ssize_t offload_recvfrom(void *obj, void *buf, size_t max_len, int flags, + struct sockaddr *src_addr, socklen_t *addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + char sendbuf[sizeof("AT+CARECV=##,####")]; + int ret, packet_size; + struct socket_read_data sock_data; + + struct modem_cmd data_cmd[] = { MODEM_CMD("+CARECV: ", on_cmd_carecv, 1U, ",") }; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_ERR("Modem currently not attached to the network!"); + return -EINVAL; + } + + if (!buf || max_len == 0) { + errno = EINVAL; + return -1; + } + + if (flags & ZSOCK_MSG_PEEK) { + errno = ENOTSUP; + return -1; + } + + packet_size = modem_socket_next_packet_size(&mdata.socket_config, sock); + if (!packet_size) { + if (flags & ZSOCK_MSG_DONTWAIT) { + errno = EAGAIN; + return -1; + } + + modem_socket_wait_data(&mdata.socket_config, sock); + packet_size = modem_socket_next_packet_size(&mdata.socket_config, sock); + } + + max_len = (max_len > MDM_MAX_DATA_LENGTH) ? MDM_MAX_DATA_LENGTH : max_len; + snprintk(sendbuf, sizeof(sendbuf), "AT+CARECV=%d,%zd", sock->id, max_len); + + memset(&sock_data, 0, sizeof(sock_data)); + sock_data.recv_buf = buf; + sock_data.recv_buf_len = max_len; + sock_data.recv_addr = src_addr; + sock->data = &sock_data; + mdata.current_sock_fd = sock->sock_fd; + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, data_cmd, ARRAY_SIZE(data_cmd), + sendbuf, &mdata.sem_response, MDM_CMD_TIMEOUT); + if (ret < 0) { + errno = -ret; + ret = -1; + goto exit; + } + + /* HACK: use dst address as src */ + if (src_addr && addrlen) { + *addrlen = sizeof(sock->dst); + memcpy(src_addr, &sock->dst, *addrlen); + } + + errno = 0; + ret = sock_data.recv_read_len; + +exit: + /* clear socket data */ + mdata.current_sock_fd = -1; + sock->data = NULL; + return ret; +} + +/* + * Sends messages to the modem. + */ +static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags) +{ + struct modem_socket *sock = obj; + ssize_t sent = 0; + const char *buf; + size_t len; + int ret; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_ERR("Modem currently not attached to the network!"); + return -EINVAL; + } + + if (sock->type == SOCK_DGRAM) { + /* + * Current implementation only handles single contiguous fragment at a time, so + * prevent sending multiple datagrams. + */ + if (msghdr_non_empty_iov_count(msg) > 1) { + errno = EMSGSIZE; + return -1; + } + } + + for (int i = 0; i < msg->msg_iovlen; i++) { + buf = msg->msg_iov[i].iov_base; + len = msg->msg_iov[i].iov_len; + + while (len > 0) { + ret = offload_sendto(obj, buf, len, flags, msg->msg_name, msg->msg_namelen); + if (ret < 0) { + return ret; + } + + sent += ret; + buf += ret; + len -= ret; + } + } + + return sent; +} + +/* + * Closes a given socket. + */ +static void socket_close(struct modem_socket *sock) +{ + char buf[sizeof("AT+CACLOSE=##")]; + int ret; + + snprintk(buf, sizeof(buf), "AT+CACLOSE=%d", sock->id); + + ret = modem_cmd_send(&mctx.iface, &mctx.cmd_handler, NULL, 0U, buf, &mdata.sem_response, + MDM_CMD_TIMEOUT); + if (ret < 0) { + LOG_ERR("%s ret: %d", buf, ret); + } + + modem_socket_put(&mdata.socket_config, sock->sock_fd); +} + +/* + * Offloads read by reading from a given socket. + */ +static ssize_t offload_read(void *obj, void *buffer, size_t count) +{ + return offload_recvfrom(obj, buffer, count, 0, NULL, 0); +} + +/* + * Offloads write by writing to a given socket. + */ +static ssize_t offload_write(void *obj, const void *buffer, size_t count) +{ + return offload_sendto(obj, buffer, count, 0, NULL, 0); +} + +/* + * Offloads close by terminating the connection and freeing the socket. + */ +static int offload_close(void *obj) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_ERR("Modem currently not attached to the network!"); + return -EINVAL; + } + + /* Make sure socket is allocated */ + if (modem_socket_is_allocated(&mdata.socket_config, sock) == false) { + return 0; + } + + socket_close(sock); + + return 0; +} + +/* + * Polls a given socket. + */ +static int offload_poll(struct zsock_pollfd *fds, int nfds, int msecs) +{ + int i; + void *obj; + + /* Modem is not attached to the network. */ + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_ERR("Modem currently not attached to the network!"); + return -EINVAL; + } + + /* Only accept modem sockets. */ + for (i = 0; i < nfds; i++) { + if (fds[i].fd < 0) { + continue; + } + + /* If vtable matches, then it's modem socket. */ + obj = zvfs_get_fd_obj(fds[i].fd, + (const struct fd_op_vtable *)&offload_socket_fd_op_vtable, + EINVAL); + if (obj == NULL) { + return -1; + } + } + + return modem_socket_poll(&mdata.socket_config, fds, nfds, msecs); +} + +/* + * Offloads ioctl. Only supported ioctl is poll_offload. + */ +static int offload_ioctl(void *obj, unsigned int request, va_list args) +{ + switch (request) { + case ZFD_IOCTL_POLL_PREPARE: + return -EXDEV; + + case ZFD_IOCTL_POLL_UPDATE: + return -EOPNOTSUPP; + + case ZFD_IOCTL_POLL_OFFLOAD: { + /* Poll on the given socket. */ + struct zsock_pollfd *fds; + int nfds, timeout; + + fds = va_arg(args, struct zsock_pollfd *); + nfds = va_arg(args, int); + timeout = va_arg(args, int); + + return offload_poll(fds, nfds, timeout); + } + + default: + errno = EINVAL; + return -1; + } +} + +const struct socket_op_vtable offload_socket_fd_op_vtable = { + .fd_vtable = { + .read = offload_read, + .write = offload_write, + .close = offload_close, + .ioctl = offload_ioctl, + }, + .bind = NULL, + .connect = offload_connect, + .sendto = offload_sendto, + .recvfrom = offload_recvfrom, + .listen = NULL, + .accept = NULL, + .sendmsg = offload_sendmsg, + .getsockopt = NULL, + .setsockopt = NULL, +}; + +void sim7080_handle_sock_data_indication(int fd) +{ + struct modem_socket *sock = modem_socket_from_fd(&mdata.socket_config, fd); + + if (!sock) { + LOG_INF("No socket with fd %d", fd); + return; + } + + /* Modem does not tell packet size. Set dummy for receive. */ + modem_socket_packet_size_update(&mdata.socket_config, sock, 1); + + LOG_INF("Data available on socket: %d", fd); + modem_socket_data_ready(&mdata.socket_config, sock); +} + +void sim7080_handle_sock_state(int fd, uint8_t state) +{ + struct modem_socket *sock = modem_socket_from_fd(&mdata.socket_config, fd); + + if (!sock) { + LOG_INF("No socket with fd %d", fd); + return; + } + + /* Only continue if socket was closed. */ + if (state != 0) { + return; + } + + LOG_INF("Socket close indication for socket: %d", fd); + + sock->is_connected = false; + + /* Unblock potentially waiting socket */ + modem_socket_packet_size_update(&mdata.socket_config, sock, 0); + modem_socket_data_ready(&mdata.socket_config, sock); +} + +int sim7080_offload_socket(int family, int type, int proto) +{ + int ret; + + ret = modem_socket_get(&mdata.socket_config, family, type, proto); + if (ret < 0) { + errno = -ret; + return -1; + } + + errno = 0; + return ret; +} + +int mdm_sim7080_start_network(void) +{ + int ret = -EALREADY; + + if (sim7080_get_state() == SIM7080_STATE_NETWORKING) { + LOG_WRN("Network already active"); + goto out; + } else if (sim7080_get_state() != SIM7080_STATE_IDLE) { + LOG_WRN("Can only activate networking from idle state"); + ret = -EINVAL; + goto out; + } + + ret = sim7080_pdp_activate(); + +out: + return ret; +} + +int mdm_sim7080_stop_network(void) +{ + int ret = -EINVAL; + + if (sim7080_get_state() != SIM7080_STATE_NETWORKING) { + LOG_WRN("Modem not in networking state"); + goto out; + } + + ret = sim7080_pdp_deactivate(); + +out: + return ret; +} diff --git a/drivers/modem/simcom/sim7080/sim7080_utils.c b/drivers/modem/simcom/sim7080/sim7080_utils.c new file mode 100644 index 0000000000000..a58745cad998f --- /dev/null +++ b/drivers/modem/simcom/sim7080/sim7080_utils.c @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 metraTec GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +LOG_MODULE_REGISTER(modem_simcom_sim7080_utils, CONFIG_MODEM_LOG_LEVEL); + +#include "sim7080.h" + +int sim7080_utils_parse_time(uint8_t *date, uint8_t *time_str, struct tm *t) +{ + char *saveptr; + int ret = -1; + + if (!date || !time_str || !t) { + ret = -EINVAL; + goto out; + } + + memset(t, 0, sizeof(*t)); + + char *tmp = strtok_r(date, "/", &saveptr); + + if (tmp == NULL) { + LOG_WRN("Failed to parse year"); + goto out; + } + + t->tm_year = (int)strtol(tmp, NULL, 10) - 1900; + + tmp = strtok_r(NULL, "/", &saveptr); + if (tmp == NULL) { + LOG_WRN("Failed to parse month"); + goto out; + } + + t->tm_mon = (int)strtol(tmp, NULL, 10) - 1; + + tmp = strtok_r(NULL, "", &saveptr); + if (tmp == NULL) { + LOG_WRN("Failed to parse day"); + goto out; + } + + t->tm_mday = (int)strtol(tmp, NULL, 10); + + tmp = strtok_r(time_str, ":", &saveptr); + if (tmp == NULL) { + LOG_WRN("Failed to parse hour"); + goto out; + } + + t->tm_hour = (int)strtol(tmp, NULL, 10); + + tmp = strtok_r(NULL, ":", &saveptr); + if (tmp == NULL) { + LOG_WRN("Failed to parse minute"); + goto out; + } + + t->tm_min = (int)strtol(tmp, NULL, 10); + + tmp = strtok_r(NULL, "+", &saveptr); + if (tmp == NULL) { + LOG_WRN("Failed to parse second"); + goto out; + } + + t->tm_sec = (int)strtol(tmp, NULL, 10); + + /* Mark dst as not available */ + t->tm_isdst = -1; + + ret = 0; +out: + return ret; +} diff --git a/include/zephyr/drivers/modem/simcom-sim7080.h b/include/zephyr/drivers/modem/simcom-sim7080.h index d6560633d4758..0bafc04a5a683 100644 --- a/include/zephyr/drivers/modem/simcom-sim7080.h +++ b/include/zephyr/drivers/modem/simcom-sim7080.h @@ -1,4 +1,6 @@ -/* +/** @file + * @brief Simcom SIM7080 modem public API header file. + * * Copyright (C) 2021 metraTec GmbH * * SPDX-License-Identifier: Apache-2.0 @@ -10,14 +12,33 @@ #include #include +#include #ifdef __cplusplus extern "C" { #endif +/** Maximum Length of GNSS UTC data */ #define SIM7080_GNSS_DATA_UTC_LEN 20 +/** Maximum SMS length */ #define SIM7080_SMS_MAX_LEN 160 +/** Maximum UE system information band size */ +#define SIM7080_UE_SYS_INFO_BAND_SIZE 32 +/** Maximum number of DNS retries */ +#define SIM7080_DNS_MAX_RECOUNT 10 +/** Maximum timeout for DNS queries in milliseconds */ +#define SIM7080_DNS_MAX_TIMEOUT_MS 60000 + +/** Sim7080 modem state */ +enum sim7080_state { + SIM7080_STATE_INIT = 0, /**< Initial modem state */ + SIM7080_STATE_IDLE, /**< Modem idle */ + SIM7080_STATE_NETWORKING, /**< Network active */ + SIM7080_STATE_GNSS, /**< GNSS active */ + SIM7080_STATE_OFF, /**< Modem off */ +}; +/** Sim7080 gnss data structure */ struct sim7080_gnss_data { /** * Whether gnss is powered or not. @@ -57,54 +78,47 @@ struct sim7080_gnss_data { uint16_t kmh; }; -/** - * Possible sms states in memory. - */ +/** Possible sms states in memory. */ enum sim7080_sms_stat { - SIM7080_SMS_STAT_REC_UNREAD = 0, - SIM7080_SMS_STAT_REC_READ, - SIM7080_SMS_STAT_STO_UNSENT, - SIM7080_SMS_STAT_STO_SENT, - SIM7080_SMS_STAT_ALL, + SIM7080_SMS_STAT_REC_UNREAD = 0, /**< Message unread */ + SIM7080_SMS_STAT_REC_READ, /**< Message read*/ + SIM7080_SMS_STAT_STO_UNSENT, /**< Message stored unsent */ + SIM7080_SMS_STAT_STO_SENT, /**< Message stored sent */ + SIM7080_SMS_STAT_ALL, /**< Status count */ }; -/** - * Possible ftp return codes. - */ +/** Possible ftp return codes. */ enum sim7080_ftp_rc { - /* Operation finished correctly. */ - SIM7080_FTP_RC_OK = 0, - /* Session finished. */ - SIM7080_FTP_RC_FINISHED, - /* An error occurred. */ - SIM7080_FTP_RC_ERROR, + SIM7080_FTP_RC_OK = 0, /**< Operation finished correctly. */ + SIM7080_FTP_RC_FINISHED, /**< Session finished. */ + SIM7080_FTP_RC_ERROR, /**< An error occurred. */ }; /** * Buffer structure for sms. */ struct sim7080_sms { - /* First octet of the sms. */ + /** First octet of the sms. */ uint8_t first_octet; - /* Message protocol identifier. */ + /** Message protocol identifier. */ uint8_t tp_pid; - /* Status of the sms in memory. */ + /** Status of the sms in memory. */ enum sim7080_sms_stat stat; - /* Index of the sms in memory. */ + /** Index of the sms in memory. */ uint16_t index; - /* Time the sms was received. */ + /** Time the sms was received. */ struct { - uint8_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; - uint8_t timezone; + uint8_t year; /**< Current Year */ + uint8_t month; /**< Month of the year */ + uint8_t day; /**< Day of the month */ + uint8_t hour; /**< Hour of the day */ + uint8_t minute; /**< Minute */ + uint8_t second; /**< Second */ + uint8_t timezone; /**< Current timezone */ } time; - /* Buffered sms. */ + /** Buffered sms. */ char data[SIM7080_SMS_MAX_LEN + 1]; - /* Length of the sms in buffer. */ + /** Length of the sms in buffer. */ uint8_t data_len; }; @@ -112,12 +126,110 @@ struct sim7080_sms { * Buffer structure for sms reads. */ struct sim7080_sms_buffer { - /* sms structures to read to. */ + /** sms structures to read to. */ struct sim7080_sms *sms; - /* Number of sms structures. */ + /** Number of sms structures. */ uint8_t nsms; }; +/** UE system mode */ +enum sim7080_ue_sys_mode { + SIM7080_UE_SYS_MODE_NO_SERVICE, /**< No service */ + SIM7080_UE_SYS_MODE_GSM, /**< GSM */ + SIM7080_UE_SYS_MODE_LTE_CAT_M1, /**< LTE CAT M1 */ + SIM7080_UE_SYS_MODE_LTE_NB_IOT, /**< LTE NB IOT */ +}; + +/** UE operating mode */ +enum sim7080_ue_op_mode { + SIM7080_UE_OP_MODE_ONLINE, /**< Online */ + SIM7080_UE_OP_MODE_OFFLINE, /**< Offline */ + SIM7080_UE_OP_MODE_FACTORY_TEST_MODE, /**< Factory test mode */ + SIM7080_UE_OP_MODE_RESET, /**< Reset */ + SIM7080_UE_OP_MODE_LOW_POWER_MODE, /**< Low power mode */ +}; + +/** + * Sim7080 ue system information structure for gsm. + */ +struct sim7080_ue_sys_info_gsm { + /** Mobile country code */ + uint16_t mcc; + /** Mobile network code */ + uint16_t mcn; + /** Location area code */ + uint16_t lac; + /** Cell ID */ + uint16_t cid; + /** Absolute radio frequency channel number */ + uint8_t arfcn[SIM7080_UE_SYS_INFO_BAND_SIZE + 1]; + /** RX level in dBm */ + int16_t rx_lvl; + /** Track LO adjust */ + int16_t track_lo_adjust; + /** C1 coefficient */ + uint16_t c1; + /** C2 coefficient */ + uint16_t c2; +}; + +/** + * Sim7080 ue system information structure for LTE. + */ +struct sim7080_ue_sys_info_lte { + /** Mobile country code */ + uint16_t mcc; + /** Mobile network code */ + uint16_t mcn; + /** Tracing area code */ + uint16_t tac; + /** Serving Cell ID */ + uint32_t sci; + /** Physical Cell ID */ + uint16_t pci; + /** Frequency band */ + uint8_t band[SIM7080_UE_SYS_INFO_BAND_SIZE + 1]; + /** E-UTRA absolute radio frequency channel number */ + uint16_t earfcn; + /** Downlink bandwidth in MHz */ + uint16_t dlbw; + /** Uplink bandwidth in MHz */ + uint16_t ulbw; + /** Reference signal received quality in dB */ + int16_t rsrq; + /** Reference signal received power in dBm */ + int16_t rsrp; + /** Received signal strength indicator in dBm */ + int16_t rssi; + /** Reference signal signal to noise ratio in dB */ + int16_t rssnr; + /** Signal to interference plus noise ratio in dB */ + int16_t sinr; +}; + +/** + * Sim7080 ue system information structure. + */ +struct sim7080_ue_sys_info { + /** Refer to sim7080_ue_sys_mode */ + enum sim7080_ue_sys_mode sys_mode; + /** Refer to sim7080_ue_op_mode */ + enum sim7080_ue_op_mode op_mode; + union { + /** Only set if sys_mode is GSM */ + struct sim7080_ue_sys_info_gsm gsm; + /** Only set if sys mode is LTE CAT-M1/NB-IOT */ + struct sim7080_ue_sys_info_lte lte; + } cell; /**< Cell information */ +}; + +/** + * Get the current state of the modem. + * + * @return The current state. + */ +enum sim7080_state mdm_sim7080_get_state(void); + /** * @brief Power on the Sim7080. * @@ -133,19 +245,72 @@ int mdm_sim7080_power_on(void); int mdm_sim7080_power_off(void); /** - * @brief Starts the modem in network operation mode. + * Forcefully reset the modem by pulling pwrkey for 15 seconds. + * @note The state of the modem may be undefined after calling + * this function. Call mdm_sim7080_power_on after force reset. + */ +void mdm_sim7080_force_reset(void); + +/** + * @brief Activates the network operation mode of the modem. * * @return 0 on success. Otherwise <0 is returned. + * @note The modem needs to be booted for this function to work. + * Concurrent use of network and gnss is not possible. */ int mdm_sim7080_start_network(void); +/** + * @brief Stops the networking operation mode of the modem. + * + * @return 0 on success. Otherwise <0 is returned. + */ +int mdm_sim7080_stop_network(void); + /** * @brief Starts the modem in gnss operation mode. * * @return 0 on success. Otherwise <0 is returned. + * @note The modem needs to be booted for this function to work. + * Concurrent use of network and gnss is not possible. */ int mdm_sim7080_start_gnss(void); +/** + * @brief Starts the modem in gnss operation mode with xtra functionality. + * + * @return 0 on success. Otherwise <0 is returned. + * @note The modem needs to be booted for this function to work. + * Concurrent use of network and gnss is not possible. + * @note If enabling xtra functionality fails a normal cold start will be performed. + */ +int mdm_sim7080_start_gnss_xtra(void); + +/** + * @brief Stops the modem gnss operation mode. + * + * @return 0 on success. Otherwise <0 is returned. + */ +int mdm_sim7080_stop_gnss(void); + +/** + * @brief Download the XTRA file for assisted gnss. + * + * @param server_id Id of the server to download XTRA file from. + * @param f_name The name of the XTRA file to download. + * @return 0 on success. Otherwise <0 is returned. + */ +int mdm_sim7080_download_xtra(uint8_t server_id, const char *f_name); + +/** + * @brief Query the validity of the XTRA file. + * + * @param diff_h Difference between the local time and the XTRA inject time in hours. + * @param duration_h Valid time of the XTRA file in hours. + * @param inject Injection time of the XTRA file. + */ +int mdm_sim7080_query_xtra_validity(int16_t *diff_h, int16_t *duration_h, struct tm *inject); + /** * @brief Query gnss position form the modem. * @@ -174,6 +339,11 @@ const char *mdm_sim7080_get_revision(void); */ const char *mdm_sim7080_get_imei(void); +/** + * Get the sim7080 iccid number. + */ +const char *mdm_sim7080_get_iccid(void); + /** * Read sms from sim module. * @@ -196,6 +366,17 @@ int mdm_sim7080_read_sms(struct sim7080_sms_buffer *buffer); */ int mdm_sim7080_delete_sms(uint16_t index); +/** + * Set the level of one of the module's GPIO pins + * + * @param gpio GPIO pin number + * @param level New logical level of the GPIO + * @return 0 on success. Otherwise -1 is returned. + * + * @note The GPIO will be configured as output implicitly. + */ +int mdm_sim7080_set_gpio(int gpio, int level); + /** * Start a ftp get session. * @@ -219,6 +400,52 @@ int mdm_sim7080_ftp_get_start(const char *server, const char *user, const char * */ int mdm_sim7080_ftp_get_read(char *dst, size_t *size); +/** + * Read voltage, charge status and battery connection level. + * + * @param bcs [out] Charge status. + * @param bcl [out] Battery connection level. + * @param voltage [out] Battery voltage in mV. + * @return 0 on success. Otherwise a negative error is returned. + */ +int mdm_sim7080_get_battery_charge(uint8_t *bcs, uint8_t *bcl, uint16_t *voltage); + +/** + * Read the ue system information + * + * @param info Destination buffer for information. + * @return 0 on success. Otherwise a negative error is returned. + */ +int mdm_sim7080_get_ue_sys_info(struct sim7080_ue_sys_info *info); + +/** + * Get the local time of the modem. + * + * @param t Time structure to fill. + * @return 0 on success. Otherwise a negative error is returned. + * @note Time is set by network. It may take some time for it to get valid. + */ +int mdm_sim7080_get_local_time(struct tm *t); + +/** + * Set the dns query lookup parameters. + * + * @param recount Number of retries per query. + * Maximum @c SIM7080_DNS_MAX_RECOUNT + * @param timeout Timeout for a dns query in milliseconds. + * Maximum @c SIM7080_DNS_MAX_TIMEOUT_MS + * @return 0 on success. Otherwise a negative error is returned. + */ +int mdm_sim7080_dns_set_lookup_params(uint8_t recount, uint16_t timeout); + +/** + * Get the dns query lookup parameters. + * + * @param recount [out] Number of retries per query. + * @param timeout [out] Timeout for a dns query in milliseconds. + */ +void mdm_sim7080_dns_get_lookup_params(uint8_t *recount, uint16_t *timeout); + #ifdef __cplusplus } #endif