diff --git a/drivers/modem/CMakeLists.txt b/drivers/modem/CMakeLists.txt index 62b84bd792946..1c3b458b1fc88 100644 --- a/drivers/modem/CMakeLists.txt +++ b/drivers/modem/CMakeLists.txt @@ -35,5 +35,7 @@ if (CONFIG_MODEM_SIM7080) zephyr_library_sources(simcom-sim7080.c) endif() +add_subdirectory_ifdef(CONFIG_MODEM_NRF91_SLM nordic) + zephyr_library_sources_ifdef(CONFIG_MODEM_CELLULAR modem_cellular.c) zephyr_library_sources_ifdef(CONFIG_MODEM_AT_SHELL modem_at_shell.c) diff --git a/drivers/modem/Kconfig b/drivers/modem/Kconfig index b8e66ef427482..1715876a28c03 100644 --- a/drivers/modem/Kconfig +++ b/drivers/modem/Kconfig @@ -195,5 +195,6 @@ source "drivers/modem/Kconfig.at_shell" source "drivers/modem/Kconfig.hl7800" source "drivers/modem/Kconfig.simcom-sim7080" +source "drivers/modem/nordic/Kconfig" endif # MODEM diff --git a/drivers/modem/nordic/CMakeLists.txt b/drivers/modem/nordic/CMakeLists.txt new file mode 100644 index 0000000000000..bfb1a58dc9df7 --- /dev/null +++ b/drivers/modem/nordic/CMakeLists.txt @@ -0,0 +1,4 @@ +# Copyright 2025 Nova Dynamics LLC +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library_sources(nrf91_slm_dns.c nrf91_slm_socket.c nrf91_slm.c) diff --git a/drivers/modem/nordic/Kconfig b/drivers/modem/nordic/Kconfig new file mode 100644 index 0000000000000..2e283b4eeb1d8 --- /dev/null +++ b/drivers/modem/nordic/Kconfig @@ -0,0 +1,33 @@ +# Copyright 2025 Nova Dynamics LLC +# SPDX-License-Identifier: Apache-2.0 + +config MODEM_NRF91_SLM + bool "Nordic nRF91x SLM Driver" + select MODEM_SOCKET + select NET_OFFLOAD + select NET_SOCKETS_OFFLOAD + select MODEM_MODULES + select MODEM_CHAT + select MODEM_PIPE + select MODEM_BACKEND_UART + select RING_BUFFER + depends on DT_HAS_NORDIC_NRF91_SLM_ENABLED + imply GPIO + help + Enables the offloaded driver for the Nordic nRF91 Serial LTE Modem. + +if MODEM_NRF91_SLM + +config MODEM_NRF91_SLM_PERIODIC_SCRIPT_MS + int "Periodic script interval in milliseconds" + default 2000 + +config MODEM_NRF91_SLM_UART_BUFFER_SIZES + int "The UART receive and transmit buffer sizes in bytes." + default 512 + +config MODEM_NRF91_SLM_CHAT_BUFFER_SIZES + int "The size of the buffers used for the chat scripts in bytes." + default 128 + +endif diff --git a/drivers/modem/nordic/nrf91_slm.c b/drivers/modem/nordic/nrf91_slm.c new file mode 100644 index 0000000000000..a1f62c34bcd9f --- /dev/null +++ b/drivers/modem/nordic/nrf91_slm.c @@ -0,0 +1,1534 @@ +/* + * Copyright 2025 Nova Dynamics LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT nordic_nrf91_slm + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(nrf91_slm, CONFIG_MODEM_LOG_LEVEL); + +#include +#include + +#include +#include + +#include "nrf91_slm.h" + +#define PERIODIC_SCRIPT_TIMEOUT K_MSEC(CONFIG_MODEM_NRF91_SLM_PERIODIC_SCRIPT_MS) + +/* Magic constants */ +#define CSQ_RSSI_UNKNOWN (99) +#define CESQ_RSRP_UNKNOWN (255) +#define CESQ_RSRQ_UNKNOWN (255) + +/* Magic numbers to units conversions */ +#define CSQ_RSSI_TO_DB(v) (-113 + (2 * (rssi))) +#define CESQ_RSRP_TO_DB(v) (-140 + (v)) +#define CESQ_RSRQ_TO_DB(v) (-20 + ((v) / 2)) + +enum nrf91_slm_event { + NRF91_SLM_EVENT_RESUME = 0, + NRF91_SLM_EVENT_SUSPEND, + NRF91_SLM_EVENT_SCRIPT_SUCCESS, + NRF91_SLM_EVENT_SCRIPT_FAILED, + NRF91_SLM_EVENT_TIMEOUT, + NRF91_SLM_EVENT_REGISTERED, + NRF91_SLM_EVENT_DEREGISTERED, + NRF91_SLM_EVENT_PPP_CONNECTED, + NRF91_SLM_EVENT_PPP_DISCONNECTED, +}; + +struct nrf91_slm_config { + const struct device *uart; + struct gpio_dt_spec power_gpio; + struct gpio_dt_spec reset_gpio; + uint16_t power_pulse_duration_ms; + uint16_t reset_pulse_duration_ms; + uint16_t startup_time_ms; + uint16_t shutdown_time_ms; + bool autostarts; +}; + +static struct nrf91_slm_data mdata; + +static const struct nrf91_slm_config mconfig = { + .uart = DEVICE_DT_GET(DT_INST_BUS(0)), + .power_gpio = GPIO_DT_SPEC_INST_GET_OR(0, mdm_power_gpios, {}), + .reset_gpio = GPIO_DT_SPEC_INST_GET_OR(0, mdm_reset_gpios, {}), + .power_pulse_duration_ms = 100, + .reset_pulse_duration_ms = 100, + .startup_time_ms = 2000, + .shutdown_time_ms = 10000, + .autostarts = false, +}; + +/****************************************************************************** + * Logging functions + *****************************************************************************/ + +#if defined(CONFIG_LOG) && (CONFIG_MODEM_LOG_LEVEL == LOG_LEVEL_DBG) + +static const char *nrf91_slm_state_str(enum nrf91_slm_state state) +{ + switch (state) { + case NRF91_SLM_STATE_IDLE: + return "idle"; + case NRF91_SLM_STATE_RESET_PULSE: + return "reset pulse"; + case NRF91_SLM_STATE_POWER_ON_PULSE: + return "power pulse"; + case NRF91_SLM_STATE_AWAIT_POWER_ON: + return "await power on"; + case NRF91_SLM_STATE_RUN_INIT_SCRIPT: + return "run init script"; + case NRF91_SLM_STATE_AWAIT_REGISTERED: + return "await registered"; + case NRF91_SLM_STATE_DISCONNECT_PPP: + return "disconnect ppp"; + case NRF91_SLM_STATE_RUN_DIAL_SCRIPT: + return "run dial script"; + case NRF91_SLM_STATE_CARRIER_ON: + return "carrier on"; + case NRF91_SLM_STATE_INIT_POWER_OFF: + return "init power off"; + case NRF91_SLM_STATE_POWER_OFF_PULSE: + return "power off pulse"; + case NRF91_SLM_STATE_AWAIT_POWER_OFF: + return "await power off"; + } + + return ""; +} + +static const char *nrf91_slm_event_str(enum nrf91_slm_event event) +{ + switch (event) { + case NRF91_SLM_EVENT_RESUME: + return "resume"; + case NRF91_SLM_EVENT_SUSPEND: + return "suspend"; + case NRF91_SLM_EVENT_SCRIPT_SUCCESS: + return "script success"; + case NRF91_SLM_EVENT_SCRIPT_FAILED: + return "script failed"; + case NRF91_SLM_EVENT_TIMEOUT: + return "timeout"; + case NRF91_SLM_EVENT_REGISTERED: + return "registered"; + case NRF91_SLM_EVENT_DEREGISTERED: + return "deregistered"; + case NRF91_SLM_EVENT_PPP_CONNECTED: + return "ppp connected"; + case NRF91_SLM_EVENT_PPP_DISCONNECTED: + return "ppp disconnected"; + } + + return ""; +} + +static void nrf91_slm_log_state_changed(enum nrf91_slm_state last_state, + enum nrf91_slm_state new_state) +{ + LOG_DBG("switch from %s to %s", nrf91_slm_state_str(last_state), + nrf91_slm_state_str(new_state)); +} + +static void nrf91_slm_log_event(enum nrf91_slm_event evt) +{ + LOG_DBG("event %s", nrf91_slm_event_str(evt)); +} + +#endif + +/****************************************************************************** + * Helper functions + *****************************************************************************/ + +static bool nrf91_slm_gpio_is_enabled(const struct gpio_dt_spec *gpio) +{ + return gpio->port != NULL; +} + +static void nrf91_slm_start_timer(struct nrf91_slm_data *data, k_timeout_t timeout) +{ + k_work_schedule(&data->timeout_work, timeout); +} + +static void nrf91_slm_stop_timer(struct nrf91_slm_data *data) +{ + k_work_cancel_delayable(&data->timeout_work); +} + +static void nrf91_slm_try_run_script(struct nrf91_slm_data *data, + const struct modem_chat_script *script) +{ + int ret = k_mutex_lock(&data->chat_lock, K_NO_WAIT); + + if (ret == 0) { + ret = modem_chat_run_script_async(&data->chat, script); + k_mutex_unlock(&data->chat_lock); + } + + if (ret < 0) { + nrf91_slm_start_timer(data, PERIODIC_SCRIPT_TIMEOUT); + } +} + +static void nrf91_slm_delegate_event(struct nrf91_slm_data *data, enum nrf91_slm_event evt) +{ + k_mutex_lock(&data->event_rb_lock, K_FOREVER); + ring_buf_put(&data->event_rb, (uint8_t *)&evt, 1); + k_mutex_unlock(&data->event_rb_lock); + k_work_submit(&data->event_dispatch_work); +} + +static bool nrf91_slm_is_registered(struct nrf91_slm_data *data) +{ + return (data->registration_status_gsm == CELLULAR_REGISTRATION_REGISTERED_HOME) || + (data->registration_status_gsm == CELLULAR_REGISTRATION_REGISTERED_ROAMING) || + (data->registration_status_gprs == CELLULAR_REGISTRATION_REGISTERED_HOME) || + (data->registration_status_gprs == CELLULAR_REGISTRATION_REGISTERED_ROAMING) || + (data->registration_status_lte == CELLULAR_REGISTRATION_REGISTERED_HOME) || + (data->registration_status_lte == CELLULAR_REGISTRATION_REGISTERED_ROAMING); +} + +/****************************************************************************** + * Modem chat callbacks + *****************************************************************************/ + +static void nrf91_slm_chat_callback_handler(struct modem_chat *chat, + enum modem_chat_script_result result, void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (result == MODEM_CHAT_SCRIPT_RESULT_SUCCESS) { + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_SCRIPT_SUCCESS); + } else { + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_SCRIPT_FAILED); + } +} + +static void nrf91_slm_chat_on_imei(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (argc != 2) { + return; + } + + strncpy(data->imei, argv[1], sizeof(data->imei) - 1); +} + +static void nrf91_slm_chat_on_cgmm(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (argc != 2) { + return; + } + + strncpy(data->model_id, argv[1], sizeof(data->model_id) - 1); +} + +static void nrf91_slm_chat_on_cgmi(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (argc != 2) { + return; + } + + strncpy(data->manufacturer, argv[1], sizeof(data->manufacturer) - 1); +} + +static void nrf91_slm_chat_on_cgmr(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (argc != 2) { + return; + } + + strncpy(data->fw_version, argv[1], sizeof(data->fw_version) - 1); +} + +static void nrf91_slm_chat_on_csq(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (argc != 3) { + return; + } + + data->rssi = (uint8_t)atoi(argv[1]); +} + +static void nrf91_slm_chat_on_cesq(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (argc != 7) { + return; + } + + data->rsrq = (uint8_t)atoi(argv[5]); + data->rsrp = (uint8_t)atoi(argv[6]); +} + +static void nrf91_slm_chat_on_cxreg(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + enum cellular_registration_status registration_status = 0; + + /* This receives both +C*REG? read command answers and unsolicited notifications. + * Their syntax differs in that the former has one more parameter, , which is first. + */ + if (argc >= 3 && argv[2][0] != '"') { + /* +CEREG: ,[,[...]] */ + registration_status = atoi(argv[2]); + } else if (argc >= 2) { + /* +CEREG: [,[...]] */ + registration_status = atoi(argv[1]); + } else { + return; + } + + if (strcmp(argv[0], "+CREG: ") == 0) { + data->registration_status_gsm = registration_status; + } else if (strcmp(argv[0], "+CGREG: ") == 0) { + data->registration_status_gprs = registration_status; + } else { /* CEREG */ + data->registration_status_lte = registration_status; + } + + if (nrf91_slm_is_registered(data)) { + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_REGISTERED); + } else { + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_DEREGISTERED); + } +} + +static void nrf91_slm_chat_on_xppp(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + int status = atoi(argv[1]); + + if (status) { + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_PPP_CONNECTED); + } else { + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_PPP_DISCONNECTED); + } +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); +MODEM_CHAT_MATCHES_DEFINE(allow_match, + MODEM_CHAT_MATCH("OK", "", NULL), + MODEM_CHAT_MATCH("ERROR", "", NULL)); + +MODEM_CHAT_MATCH_DEFINE(imei_match, "", "", nrf91_slm_chat_on_imei); +MODEM_CHAT_MATCH_DEFINE(cgmm_match, "", "", nrf91_slm_chat_on_cgmm); +MODEM_CHAT_MATCH_DEFINE(cgmi_match, "", "", nrf91_slm_chat_on_cgmi); +MODEM_CHAT_MATCH_DEFINE(cgmr_match, "", "", nrf91_slm_chat_on_cgmr); +MODEM_CHAT_MATCH_DEFINE(csq_match, "+CSQ: ", ",", nrf91_slm_chat_on_csq); +MODEM_CHAT_MATCH_DEFINE(cesq_match, "+CESQ: ", ",", nrf91_slm_chat_on_cesq); + +MODEM_CHAT_MATCHES_DEFINE(unsol_matches, + MODEM_CHAT_MATCH("+CREG: ", ",", nrf91_slm_chat_on_cxreg), + MODEM_CHAT_MATCH("+CEREG: ", ",", nrf91_slm_chat_on_cxreg), + MODEM_CHAT_MATCH("+CGREG: ", ",", nrf91_slm_chat_on_cxreg), + MODEM_CHAT_MATCH("#XPPP: ", ",", nrf91_slm_chat_on_xppp)); + +MODEM_CHAT_MATCHES_DEFINE(abort_matches, MODEM_CHAT_MATCH("ERROR", "", NULL)); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(nordic_nrf91_slm_init_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP_MULT("AT", allow_match), + MODEM_CHAT_SCRIPT_CMD_RESP_MULT("AT+CMEE=0", allow_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG=1", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGSN", imei_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGMM", cgmm_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGMI", cgmi_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGMR", cgmr_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(nordic_nrf91_slm_init_chat_script, nordic_nrf91_slm_init_chat_script_cmds, + abort_matches, nrf91_slm_chat_callback_handler, 10); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(nordic_nrf91_slm_dial_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CFUN=4", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CFUN=1", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(nordic_nrf91_slm_dial_chat_script, nordic_nrf91_slm_dial_chat_script_cmds, + abort_matches, nrf91_slm_chat_callback_handler, 10); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(nordic_nrf91_slm_periodic_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG?", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(nordic_nrf91_slm_periodic_chat_script, + nordic_nrf91_slm_periodic_chat_script_cmds, abort_matches, + nrf91_slm_chat_callback_handler, 4); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(nordic_nrf91_slm_ppp_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT#XPPP=0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT#XPPP?", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(nordic_nrf91_slm_ppp_chat_script, nordic_nrf91_slm_ppp_chat_script_cmds, + abort_matches, nrf91_slm_chat_callback_handler, 10); + +/****************************************************************************** + * Modem state machine + *****************************************************************************/ + +static void nrf91_slm_enter_state(struct nrf91_slm_data *data, enum nrf91_slm_state state); + +static void nrf91_slm_timeout_handler(struct k_work *item) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(item); + struct nrf91_slm_data *data = CONTAINER_OF(dwork, struct nrf91_slm_data, timeout_work); + + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_TIMEOUT); +} + +static void nrf91_slm_begin_power_off_pulse(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + if (nrf91_slm_gpio_is_enabled(&config->power_gpio)) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_POWER_OFF_PULSE); + } else { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + } +} + +static int nrf91_slm_on_idle_state_enter(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + int ret; + + if (nrf91_slm_gpio_is_enabled(&config->reset_gpio)) { + gpio_pin_set_dt(&config->reset_gpio, 1); + } + + modem_chat_release(&data->chat); + ret = modem_pipe_close(data->uart_pipe, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("failed to close pipe"); + } + + data->registration_status_gsm = CELLULAR_REGISTRATION_NOT_REGISTERED; + data->registration_status_gprs = CELLULAR_REGISTRATION_NOT_REGISTERED; + data->registration_status_lte = CELLULAR_REGISTRATION_NOT_REGISTERED; + + k_sem_give(&data->suspended_sem); + return ret; +} + +static void nrf91_slm_idle_event_handler(struct nrf91_slm_data *data, enum nrf91_slm_event evt) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + int ret; + + switch (evt) { + case NRF91_SLM_EVENT_RESUME: + ret = modem_pipe_open(data->uart_pipe, K_SECONDS(2)); + if (ret < 0) { + LOG_ERR("failed to open pipe"); + return; + } + + modem_chat_attach(&data->chat, data->uart_pipe); + + if (config->autostarts) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_AWAIT_POWER_ON); + break; + } + + if (nrf91_slm_gpio_is_enabled(&config->power_gpio)) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_POWER_ON_PULSE); + break; + } + + if (nrf91_slm_gpio_is_enabled(&config->reset_gpio)) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_AWAIT_POWER_ON); + break; + } + + nrf91_slm_enter_state(data, NRF91_SLM_STATE_RUN_INIT_SCRIPT); + break; + + case NRF91_SLM_EVENT_SUSPEND: + k_sem_give(&data->suspended_sem); + break; + + default: + break; + } +} + +static int nrf91_slm_on_idle_state_leave(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + k_sem_take(&data->suspended_sem, K_NO_WAIT); + + if (nrf91_slm_gpio_is_enabled(&config->reset_gpio)) { + gpio_pin_set_dt(&config->reset_gpio, 0); + } + + return 0; +} + +static int nrf91_slm_on_reset_pulse_state_enter(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + gpio_pin_set_dt(&config->reset_gpio, 1); + nrf91_slm_start_timer(data, K_MSEC(config->reset_pulse_duration_ms)); + return 0; +} + +static void nrf91_slm_reset_pulse_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_AWAIT_POWER_ON); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + break; + + default: + break; + } +} + +static int nrf91_slm_on_reset_pulse_state_leave(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + gpio_pin_set_dt(&config->reset_gpio, 0); + nrf91_slm_stop_timer(data); + return 0; +} + +static int nrf91_slm_on_power_on_pulse_state_enter(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + gpio_pin_set_dt(&config->power_gpio, 1); + nrf91_slm_start_timer(data, K_MSEC(config->power_pulse_duration_ms)); + return 0; +} + +static void nrf91_slm_power_on_pulse_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_AWAIT_POWER_ON); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + break; + + default: + break; + } +} + +static int nrf91_slm_on_power_on_pulse_state_leave(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + gpio_pin_set_dt(&config->power_gpio, 0); + nrf91_slm_stop_timer(data); + return 0; +} + +static int nrf91_slm_on_await_power_on_state_enter(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + nrf91_slm_start_timer(data, K_MSEC(config->startup_time_ms)); + return 0; +} + +static void nrf91_slm_await_power_on_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_RUN_INIT_SCRIPT); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + break; + + default: + break; + } +} + +static int nrf91_slm_on_run_init_script_state_enter(struct nrf91_slm_data *data) +{ + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_init_chat_script); + return 0; +} + +static void nrf91_slm_run_init_script_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + switch (evt) { + case NRF91_SLM_EVENT_SCRIPT_SUCCESS: + net_if_set_link_addr(data->netif, data->imei, ARRAY_SIZE(data->imei), + NET_LINK_UNKNOWN); + + nrf91_slm_enter_state(data, NRF91_SLM_STATE_RUN_DIAL_SCRIPT); + break; + + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_init_chat_script); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + break; + + case NRF91_SLM_EVENT_SCRIPT_FAILED: + if (nrf91_slm_gpio_is_enabled(&config->power_gpio)) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_POWER_ON_PULSE); + break; + } + + if (nrf91_slm_gpio_is_enabled(&config->reset_gpio)) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_RESET_PULSE); + break; + } + + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + break; + + default: + break; + } +} + +static int nrf91_slm_on_run_dial_script_state_enter(struct nrf91_slm_data *data) +{ + /* Allow modem time to enter command mode before running dial script */ + nrf91_slm_start_timer(data, K_MSEC(100)); + return 0; +} + +static void nrf91_slm_run_dial_script_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_SCRIPT_SUCCESS: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_AWAIT_REGISTERED); + break; + + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_dial_chat_script); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_INIT_POWER_OFF); + break; + + default: + break; + } +} + +static int nrf91_slm_on_await_registered_state_enter(struct nrf91_slm_data *data) +{ + nrf91_slm_start_timer(data, PERIODIC_SCRIPT_TIMEOUT); + return 0; +} + +static void nrf91_slm_await_registered_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_SCRIPT_SUCCESS: + case NRF91_SLM_EVENT_SCRIPT_FAILED: + nrf91_slm_start_timer(data, PERIODIC_SCRIPT_TIMEOUT); + break; + + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_periodic_chat_script); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_INIT_POWER_OFF); + break; + + case NRF91_SLM_EVENT_REGISTERED: + case NRF91_SLM_EVENT_PPP_CONNECTED: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_DISCONNECT_PPP); + break; + + default: + break; + } +} + +static int nrf91_slm_on_disconnect_ppp_state_enter(struct nrf91_slm_data *data) +{ + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_ppp_chat_script); + return 0; +} + +static void nrf91_slm_disconnect_ppp_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_SCRIPT_SUCCESS: + nrf91_slm_start_timer(data, PERIODIC_SCRIPT_TIMEOUT); + break; + + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_ppp_chat_script); + break; + + case NRF91_SLM_EVENT_REGISTERED: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_CARRIER_ON); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_INIT_POWER_OFF); + break; + + case NRF91_SLM_EVENT_SCRIPT_FAILED: + case NRF91_SLM_EVENT_PPP_DISCONNECTED: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_CARRIER_ON); + break; + + default: + break; + } +} + +static int nrf91_slm_on_await_registered_state_leave(struct nrf91_slm_data *data) +{ + nrf91_slm_stop_timer(data); + return 0; +} + +static int nrf91_slm_on_carrier_on_state_enter(struct nrf91_slm_data *data) +{ + net_if_carrier_on(data->netif); + nrf91_slm_start_timer(data, PERIODIC_SCRIPT_TIMEOUT); + return 0; +} + +static void nrf91_slm_carrier_on_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + switch (evt) { + case NRF91_SLM_EVENT_SCRIPT_SUCCESS: + case NRF91_SLM_EVENT_SCRIPT_FAILED: + nrf91_slm_start_timer(data, PERIODIC_SCRIPT_TIMEOUT); + break; + + case NRF91_SLM_EVENT_TIMEOUT: + nrf91_slm_try_run_script(data, &nordic_nrf91_slm_periodic_chat_script); + break; + + case NRF91_SLM_EVENT_DEREGISTERED: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_RUN_DIAL_SCRIPT); + break; + + case NRF91_SLM_EVENT_SUSPEND: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_INIT_POWER_OFF); + break; + + case NRF91_SLM_EVENT_PPP_CONNECTED: + nrf91_slm_enter_state(data, NRF91_SLM_STATE_DISCONNECT_PPP); + break; + + default: + break; + } +} + +static int nrf91_slm_on_carrier_on_state_leave(struct nrf91_slm_data *data) +{ + nrf91_slm_stop_timer(data); + net_if_carrier_off(data->netif); + modem_chat_release(&data->chat); + return 0; +} + +static int nrf91_slm_on_init_power_off_state_enter(struct nrf91_slm_data *data) +{ + nrf91_slm_start_timer(data, K_MSEC(2000)); + return 0; +} + +static void nrf91_slm_init_power_off_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + if (evt == NRF91_SLM_EVENT_TIMEOUT) { + nrf91_slm_begin_power_off_pulse(data); + } +} + +static int nrf91_slm_on_power_off_pulse_state_enter(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + gpio_pin_set_dt(&config->power_gpio, 1); + nrf91_slm_start_timer(data, K_MSEC(config->power_pulse_duration_ms)); + return 0; +} + +static void nrf91_slm_power_off_pulse_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + if (evt == NRF91_SLM_EVENT_TIMEOUT) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_AWAIT_POWER_OFF); + } +} + +static int nrf91_slm_on_power_off_pulse_state_leave(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + gpio_pin_set_dt(&config->power_gpio, 0); + nrf91_slm_stop_timer(data); + return 0; +} + +static int nrf91_slm_on_await_power_off_state_enter(struct nrf91_slm_data *data) +{ + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)data->dev->config; + + nrf91_slm_start_timer(data, K_MSEC(config->shutdown_time_ms)); + return 0; +} + +static void nrf91_slm_await_power_off_event_handler(struct nrf91_slm_data *data, + enum nrf91_slm_event evt) +{ + if (evt == NRF91_SLM_EVENT_TIMEOUT) { + nrf91_slm_enter_state(data, NRF91_SLM_STATE_IDLE); + } +} + +static int nrf91_slm_on_state_enter(struct nrf91_slm_data *data) +{ + int ret; + + switch (data->state) { + case NRF91_SLM_STATE_IDLE: + ret = nrf91_slm_on_idle_state_enter(data); + break; + + case NRF91_SLM_STATE_RESET_PULSE: + ret = nrf91_slm_on_reset_pulse_state_enter(data); + break; + + case NRF91_SLM_STATE_POWER_ON_PULSE: + ret = nrf91_slm_on_power_on_pulse_state_enter(data); + break; + + case NRF91_SLM_STATE_AWAIT_POWER_ON: + ret = nrf91_slm_on_await_power_on_state_enter(data); + break; + + case NRF91_SLM_STATE_RUN_INIT_SCRIPT: + ret = nrf91_slm_on_run_init_script_state_enter(data); + break; + + case NRF91_SLM_STATE_RUN_DIAL_SCRIPT: + ret = nrf91_slm_on_run_dial_script_state_enter(data); + break; + + case NRF91_SLM_STATE_AWAIT_REGISTERED: + ret = nrf91_slm_on_await_registered_state_enter(data); + break; + + case NRF91_SLM_STATE_DISCONNECT_PPP: + ret = nrf91_slm_on_disconnect_ppp_state_enter(data); + break; + + case NRF91_SLM_STATE_CARRIER_ON: + ret = nrf91_slm_on_carrier_on_state_enter(data); + break; + + case NRF91_SLM_STATE_INIT_POWER_OFF: + ret = nrf91_slm_on_init_power_off_state_enter(data); + break; + + case NRF91_SLM_STATE_POWER_OFF_PULSE: + ret = nrf91_slm_on_power_off_pulse_state_enter(data); + break; + + case NRF91_SLM_STATE_AWAIT_POWER_OFF: + ret = nrf91_slm_on_await_power_off_state_enter(data); + break; + + default: + ret = 0; + break; + } + + return ret; +} + +static int nrf91_slm_on_state_leave(struct nrf91_slm_data *data) +{ + int ret; + + switch (data->state) { + case NRF91_SLM_STATE_IDLE: + ret = nrf91_slm_on_idle_state_leave(data); + break; + + case NRF91_SLM_STATE_RESET_PULSE: + ret = nrf91_slm_on_reset_pulse_state_leave(data); + break; + + case NRF91_SLM_STATE_POWER_ON_PULSE: + ret = nrf91_slm_on_power_on_pulse_state_leave(data); + break; + + case NRF91_SLM_STATE_AWAIT_REGISTERED: + ret = nrf91_slm_on_await_registered_state_leave(data); + break; + + case NRF91_SLM_STATE_CARRIER_ON: + ret = nrf91_slm_on_carrier_on_state_leave(data); + break; + + case NRF91_SLM_STATE_POWER_OFF_PULSE: + ret = nrf91_slm_on_power_off_pulse_state_leave(data); + break; + + default: + ret = 0; + break; + } + + return ret; +} + +static void nrf91_slm_enter_state(struct nrf91_slm_data *data, enum nrf91_slm_state state) +{ + int ret; + + ret = nrf91_slm_on_state_leave(data); + + if (ret < 0) { + LOG_WRN("failed to leave state, error: %i", ret); + + return; + } + + data->state = state; + ret = nrf91_slm_on_state_enter(data); + + if (ret < 0) { + LOG_WRN("failed to enter state error: %i", ret); + } +} + +static void nrf91_slm_event_handler(struct nrf91_slm_data *data, enum nrf91_slm_event evt) +{ + enum nrf91_slm_state state; + + state = data->state; + +#if defined(CONFIG_LOG) && (CONFIG_MODEM_LOG_LEVEL == LOG_LEVEL_DBG) + nrf91_slm_log_event(evt); +#endif + + switch (data->state) { + case NRF91_SLM_STATE_IDLE: + nrf91_slm_idle_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_RESET_PULSE: + nrf91_slm_reset_pulse_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_POWER_ON_PULSE: + nrf91_slm_power_on_pulse_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_AWAIT_POWER_ON: + nrf91_slm_await_power_on_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_RUN_INIT_SCRIPT: + nrf91_slm_run_init_script_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_RUN_DIAL_SCRIPT: + nrf91_slm_run_dial_script_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_AWAIT_REGISTERED: + nrf91_slm_await_registered_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_DISCONNECT_PPP: + nrf91_slm_disconnect_ppp_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_CARRIER_ON: + nrf91_slm_carrier_on_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_INIT_POWER_OFF: + nrf91_slm_init_power_off_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_POWER_OFF_PULSE: + nrf91_slm_power_off_pulse_event_handler(data, evt); + break; + + case NRF91_SLM_STATE_AWAIT_POWER_OFF: + nrf91_slm_await_power_off_event_handler(data, evt); + break; + default: + LOG_WRN("unhandled state: %d", data->state); + break; + } + +#if defined(CONFIG_LOG) && (CONFIG_MODEM_LOG_LEVEL == LOG_LEVEL_DBG) + if (state != data->state) { + nrf91_slm_log_state_changed(state, data->state); + } +#endif +} + +static void nrf91_slm_event_dispatch_handler(struct k_work *item) +{ + struct nrf91_slm_data *data = + CONTAINER_OF(item, struct nrf91_slm_data, event_dispatch_work); + + uint8_t events[sizeof(data->event_buf)]; + uint8_t events_cnt; + + k_mutex_lock(&data->event_rb_lock, K_FOREVER); + + events_cnt = (uint8_t)ring_buf_get(&data->event_rb, events, sizeof(data->event_buf)); + + k_mutex_unlock(&data->event_rb_lock); + + for (uint8_t i = 0; i < events_cnt; i++) { + nrf91_slm_event_handler(data, (enum nrf91_slm_event)events[i]); + } +} + +/****************************************************************************** + * Cellular API + *****************************************************************************/ + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(get_signal_csq_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CSQ", csq_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(get_signal_csq_chat_script, get_signal_csq_chat_script_cmds, abort_matches, + nrf91_slm_chat_callback_handler, 2); + +static inline int nrf91_slm_csq_parse_rssi(uint8_t rssi, int16_t *value) +{ + /* AT+CSQ returns a response +CSQ: , where: + * - rssi is a integer from 0 to 31 whose values describes a signal strength + * between -113 dBm for 0 and -51dbM for 31 or unknown for 99 + * - ber is an integer from 0 to 7 that describes the error rate, it can also + * be 99 for an unknown error rate + */ + if (rssi == CSQ_RSSI_UNKNOWN) { + return -EINVAL; + } + + *value = (int16_t)CSQ_RSSI_TO_DB(rssi); + return 0; +} + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(get_signal_cesq_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CESQ", cesq_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(get_signal_cesq_chat_script, get_signal_cesq_chat_script_cmds, + abort_matches, nrf91_slm_chat_callback_handler, 2); + +static inline int nrf91_slm_cesq_parse_rsrp(uint8_t rsrp, int16_t *value) +{ + if (rsrp == CESQ_RSRP_UNKNOWN) { + return -EINVAL; + } + + *value = (int16_t)CESQ_RSRP_TO_DB(rsrp); + return 0; +} + +static inline int nrf91_slm_cesq_parse_rsrq(uint8_t rsrq, int16_t *value) +{ + if (rsrq == CESQ_RSRQ_UNKNOWN) { + return -EINVAL; + } + + *value = (int16_t)CESQ_RSRQ_TO_DB(rsrq); + return 0; +} + +static int nrf91_slm_get_signal(const struct device *dev, const enum cellular_signal_type type, + int16_t *value) +{ + int ret = -ENOTSUP; + struct nrf91_slm_data *data = (struct nrf91_slm_data *)dev->data; + + if ((data->state != NRF91_SLM_STATE_AWAIT_REGISTERED) && + (data->state != NRF91_SLM_STATE_CARRIER_ON)) { + return -ENODATA; + } + + /* Run chat script */ + switch (type) { + case CELLULAR_SIGNAL_RSSI: + ret = modem_chat_run_script(&data->chat, &get_signal_csq_chat_script); + break; + + case CELLULAR_SIGNAL_RSRP: + case CELLULAR_SIGNAL_RSRQ: + ret = modem_chat_run_script(&data->chat, &get_signal_cesq_chat_script); + break; + + default: + ret = -ENOTSUP; + break; + } + + /* Verify chat script ran successfully */ + if (ret < 0) { + return ret; + } + + /* Parse received value */ + switch (type) { + case CELLULAR_SIGNAL_RSSI: + ret = nrf91_slm_csq_parse_rssi(data->rssi, value); + break; + + case CELLULAR_SIGNAL_RSRP: + ret = nrf91_slm_cesq_parse_rsrp(data->rsrp, value); + break; + + case CELLULAR_SIGNAL_RSRQ: + ret = nrf91_slm_cesq_parse_rsrq(data->rsrq, value); + break; + + default: + ret = -ENOTSUP; + break; + } + + return ret; +} + +static int nrf91_slm_get_modem_info(const struct device *dev, enum cellular_modem_info_type type, + char *info, size_t size) +{ + int ret = 0; + struct nrf91_slm_data *data = (struct nrf91_slm_data *)dev->data; + + switch (type) { + case CELLULAR_MODEM_INFO_IMEI: + strncpy(info, &data->imei[0], MIN(size, sizeof(data->imei))); + break; + case CELLULAR_MODEM_INFO_SIM_IMSI: + strncpy(info, &data->imsi[0], MIN(size, sizeof(data->imsi))); + break; + case CELLULAR_MODEM_INFO_MANUFACTURER: + strncpy(info, &data->manufacturer[0], MIN(size, sizeof(data->manufacturer))); + break; + case CELLULAR_MODEM_INFO_FW_VERSION: + strncpy(info, &data->fw_version[0], MIN(size, sizeof(data->fw_version))); + break; + case CELLULAR_MODEM_INFO_MODEL_ID: + strncpy(info, &data->model_id[0], MIN(size, sizeof(data->model_id))); + break; + case CELLULAR_MODEM_INFO_SIM_ICCID: + strncpy(info, &data->iccid[0], MIN(size, sizeof(data->iccid))); + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int nrf91_slm_get_registration_status(const struct device *dev, + enum cellular_access_technology tech, + enum cellular_registration_status *status) +{ + int ret = 0; + struct nrf91_slm_data *data = (struct nrf91_slm_data *)dev->data; + + if (data->state != NRF91_SLM_STATE_CARRIER_ON) { + return -EAGAIN; + } + + switch (tech) { + case CELLULAR_ACCESS_TECHNOLOGY_GSM: + *status = data->registration_status_gsm; + break; + case CELLULAR_ACCESS_TECHNOLOGY_GPRS: + case CELLULAR_ACCESS_TECHNOLOGY_UMTS: + case CELLULAR_ACCESS_TECHNOLOGY_EDGE: + *status = data->registration_status_gprs; + break; + case CELLULAR_ACCESS_TECHNOLOGY_LTE: + case CELLULAR_ACCESS_TECHNOLOGY_LTE_CAT_M1: + case CELLULAR_ACCESS_TECHNOLOGY_LTE_CAT_M2: + case CELLULAR_ACCESS_TECHNOLOGY_NB_IOT: + *status = data->registration_status_lte; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static DEVICE_API(cellular, nrf91_slm_api) = { + .get_signal = nrf91_slm_get_signal, + .get_modem_info = nrf91_slm_get_modem_info, + .get_registration_status = nrf91_slm_get_registration_status, +}; + +/****************************************************************************** + * Power Management + *****************************************************************************/ + +#ifdef CONFIG_PM_DEVICE +static int nrf91_slm_pm_action(const struct device *dev, enum pm_device_action action) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)dev->data; + int ret; + + switch (action) { + case PM_DEVICE_ACTION_RESUME: + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_RESUME); + ret = 0; + break; + + case PM_DEVICE_ACTION_SUSPEND: + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_SUSPEND); + ret = k_sem_take(&data->suspended_sem, K_SECONDS(30)); + break; + + default: + ret = -ENOTSUP; + break; + } + + return ret; +} + +PM_DEVICE_DT_INST_DEFINE(0, nrf91_slm_pm_action); + +#endif /* CONFIG_PM_DEVICE */ + +/****************************************************************************** + * Device Initialization + *****************************************************************************/ + +static void nrf91_slm_init_pipe(const struct device *dev) +{ + const struct nrf91_slm_config *config = dev->config; + struct nrf91_slm_data *data = dev->data; + + const struct modem_backend_uart_config uart_backend_config = { + .uart = config->uart, + .receive_buf = data->uart_backend_receive_buf, + .receive_buf_size = ARRAY_SIZE(data->uart_backend_receive_buf), + .transmit_buf = data->uart_backend_transmit_buf, + .transmit_buf_size = ARRAY_SIZE(data->uart_backend_transmit_buf), + }; + + data->uart_pipe = modem_backend_uart_init(&data->uart_backend, &uart_backend_config); +} + +static int nrf91_slm_init_chat(const struct device *dev) +{ + struct nrf91_slm_data *data = dev->data; + + const struct modem_chat_config chat_config = { + .user_data = data, + .receive_buf = data->chat_receive_buf, + .receive_buf_size = ARRAY_SIZE(data->chat_receive_buf), + .delimiter = data->chat_delimiter, + .delimiter_size = strlen(data->chat_delimiter), + .filter = data->chat_filter, + .filter_size = data->chat_filter ? strlen(data->chat_filter) : 0, + .argv = data->chat_argv, + .argv_size = ARRAY_SIZE(data->chat_argv), + .unsol_matches = unsol_matches, + .unsol_matches_size = ARRAY_SIZE(unsol_matches), + }; + + return modem_chat_init(&data->chat, &chat_config); +} + +static int nrf91_slm_init(const struct device *dev) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)dev->data; + const struct nrf91_slm_config *config = (const struct nrf91_slm_config *)dev->config; + int ret; + + data->dev = dev; + data->chat_delimiter = "\r\n"; + + k_work_init_delayable(&data->timeout_work, nrf91_slm_timeout_handler); + + k_work_init(&data->event_dispatch_work, nrf91_slm_event_dispatch_handler); + ring_buf_init(&data->event_rb, sizeof(data->event_buf), data->event_buf); + ring_buf_init(&data->sock_recv_rb, sizeof(data->sock_recv_rb_buf), data->sock_recv_rb_buf); + + k_sem_init(&data->suspended_sem, 0, 1); + k_sem_init(&data->sock_recv_sem, 0, 1); + k_sem_init(&data->sock_send_sem, 0, 1); + + if (nrf91_slm_gpio_is_enabled(&config->power_gpio)) { + ret = gpio_pin_configure_dt(&config->power_gpio, GPIO_OUTPUT_INACTIVE); + if (ret < 0) { + LOG_ERR("failed to configure power GPIO (%d)", ret); + return ret; + } + } + + if (nrf91_slm_gpio_is_enabled(&config->reset_gpio)) { + ret = gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_ACTIVE); + if (ret < 0) { + LOG_ERR("failed to configure reset GPIO (%d)", ret); + return ret; + } + } + + nrf91_slm_init_pipe(dev); + + ret = nrf91_slm_init_chat(dev); + if (ret < 0) { + LOG_ERR("failed to initialize chat (%d)", ret); + return ret; + } + +#ifndef CONFIG_PM_DEVICE + nrf91_slm_delegate_event(data, NRF91_SLM_EVENT_RESUME); +#else + pm_device_init_suspended(dev); +#endif /* CONFIG_PM_DEVICE */ + + return 0; +} + +DEVICE_DT_INST_DEFINE(0, nrf91_slm_init, PM_DEVICE_DT_INST_GET(0), &mdata, &mconfig, POST_KERNEL, + 99, &nrf91_slm_api); + +/****************************************************************************** + * Offload API + ******************************************************************************/ + +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 && proto != IPPROTO_TLS_1_2) { + return false; + } + + return true; +} + +static int offload_socket(int family, int type, int proto) +{ + return nrf91_slm_socket(&mdata, family, type, proto); +} + +static ssize_t offload_read(void *obj, void *buf, size_t count) +{ + return nrf91_slm_recvfrom(&mdata, obj, buf, count, 0, NULL, NULL); +} + +static ssize_t offload_write(void *obj, const void *buf, size_t count) +{ + return nrf91_slm_sendto(&mdata, obj, buf, count, 0, NULL, 0); +} + +static int offload_close(void *obj) +{ + return nrf91_slm_close(&mdata, obj); +} + +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: { + 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 nrf91_slm_poll(&mdata, fds, nfds, timeout); + } + default: + errno = EINVAL; + return -1; + } +} + +static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t addrlen) +{ + return nrf91_slm_connect(&mdata, obj, addr, addrlen); +} + +static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags, + const struct sockaddr *dest_addr, socklen_t addrlen) +{ + return nrf91_slm_sendto(&mdata, obj, buf, len, flags, dest_addr, addrlen); +} + +static ssize_t offload_recvfrom(void *obj, void *buf, size_t max_len, int flags, + struct sockaddr *src_addr, socklen_t *addrlen) +{ + return nrf91_slm_recvfrom(&mdata, obj, buf, max_len, flags, src_addr, addrlen); +} + +static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags) +{ + ssize_t sent = 0; + const char *buf; + size_t len; + int ret; + + 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 = nrf91_slm_sendto(&mdata, obj, buf, len, flags, msg->msg_name, + msg->msg_namelen); + + if (ret < 0) { + if (ret == -EAGAIN) { + k_sleep(K_SECONDS(1)); + } else { + return ret; + } + } else { + sent += ret; + buf += ret; + len -= ret; + } + } + } + + return sent; +} + +static int offload_getaddrinfo(const char *node, const char *service, + const struct zsock_addrinfo *hints, struct zsock_addrinfo **res) +{ + return nrf91_slm_getaddrinfo(&mdata, node, service, hints, res); +} + +static void offload_freeaddrinfo(struct zsock_addrinfo *res) +{ + nrf91_slm_freeaddrinfo(&mdata, res); +} + +static const struct socket_op_vtable offload_socket_fd_op_vtable = { + .fd_vtable = { + .read = offload_read, + .write = offload_write, + .close = offload_close, + .ioctl = offload_ioctl, + }, + .bind = NULL, + .connect = offload_connect, + .sendto = offload_sendto, + .recvfrom = offload_recvfrom, + .listen = NULL, + .accept = NULL, + .sendmsg = offload_sendmsg, + .getsockopt = NULL, + .setsockopt = NULL, +}; + +static const struct socket_dns_offload offload_dns_ops = { + .getaddrinfo = offload_getaddrinfo, + .freeaddrinfo = offload_freeaddrinfo, +}; + +static void modem_net_iface_init(struct net_if *iface) +{ + const struct device *dev = net_if_get_device(iface); + struct nrf91_slm_data *data = dev->data; + + net_if_set_link_addr(iface, data->imei, ARRAY_SIZE(data->imei), NET_LINK_UNKNOWN); + + data->netif = iface; + + modem_socket_init(&data->socket_config, &data->sockets[0], ARRAY_SIZE(data->sockets), 0, + false, &offload_socket_fd_op_vtable); + + socket_offload_dns_register(&offload_dns_ops); + + net_if_socket_offload_set(iface, offload_socket); +} + +static struct offloaded_if_api api_funcs = { + .iface_api.init = modem_net_iface_init, +}; + +NET_DEVICE_OFFLOAD_INIT(nrf91_slm_net_dev, "nrf91_slm_net_dev", NULL, PM_DEVICE_DT_INST_GET(0), + &mdata, &mconfig, 98, &api_funcs, 1500); + +NET_SOCKET_OFFLOAD_REGISTER(nrf91_slm_sock, CONFIG_NET_SOCKETS_OFFLOAD_PRIORITY, AF_UNSPEC, + offload_is_supported, offload_socket); diff --git a/drivers/modem/nordic/nrf91_slm.h b/drivers/modem/nordic/nrf91_slm.h new file mode 100644 index 0000000000000..bf8ba77e617d7 --- /dev/null +++ b/drivers/modem/nordic/nrf91_slm.h @@ -0,0 +1,138 @@ +/* + * Copyright 2025 Nova Dynamics LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef MODEM_NORDIC_NRF91_SLM_H_ +#define MODEM_NORDIC_NRF91_SLM_H_ + +#include +#include +#include +#include + +#include "../modem_socket.h" + +#define NRF91_SLM_IMEI_LEN (16) +#define NRF91_SLM_MODEL_ID_LEN (65) +#define NRF91_SLM_IMSI_LEN (23) +#define NRF91_SLM_ICCID_LEN (22) +#define NRF91_SLM_MANUFACTURER_LEN (65) +#define NRF91_SLM_FW_VERSION_LEN (65) + +#ifdef __cplusplus +extern "C" { +#endif + +enum nrf91_slm_state { + NRF91_SLM_STATE_IDLE = 0, + NRF91_SLM_STATE_RESET_PULSE, + NRF91_SLM_STATE_POWER_ON_PULSE, + NRF91_SLM_STATE_AWAIT_POWER_ON, + NRF91_SLM_STATE_RUN_INIT_SCRIPT, + NRF91_SLM_STATE_RUN_DIAL_SCRIPT, + NRF91_SLM_STATE_AWAIT_REGISTERED, + NRF91_SLM_STATE_DISCONNECT_PPP, + NRF91_SLM_STATE_CARRIER_ON, + NRF91_SLM_STATE_INIT_POWER_OFF, + NRF91_SLM_STATE_POWER_OFF_PULSE, + NRF91_SLM_STATE_AWAIT_POWER_OFF, +}; + +struct nrf91_slm_data { + /* UART backend */ + struct modem_pipe *uart_pipe; + struct modem_backend_uart uart_backend; + uint8_t uart_backend_receive_buf[CONFIG_MODEM_NRF91_SLM_UART_BUFFER_SIZES]; + uint8_t uart_backend_transmit_buf[CONFIG_MODEM_NRF91_SLM_UART_BUFFER_SIZES]; + + /* Modem chat */ + struct modem_chat chat; + uint8_t chat_receive_buf[CONFIG_MODEM_NRF91_SLM_CHAT_BUFFER_SIZES]; + uint8_t *chat_delimiter; + uint8_t *chat_filter; + uint8_t *chat_argv[32]; + struct k_mutex chat_lock; + + /* Socket chat script */ + uint8_t sock_receive_buf[CONFIG_MODEM_NRF91_SLM_UART_BUFFER_SIZES]; + uint8_t sock_recv_rb_buf[CONFIG_MODEM_NRF91_SLM_UART_BUFFER_SIZES]; + struct ring_buf sock_recv_rb; + struct k_sem sock_recv_sem; + struct k_sem sock_send_sem; + const uint8_t *sock_send_buf; + size_t sock_send_buf_len; + size_t sock_send_count; + + /* Status */ + enum cellular_registration_status registration_status_gsm; + enum cellular_registration_status registration_status_gprs; + enum cellular_registration_status registration_status_lte; + uint8_t rssi; + uint8_t rsrp; + uint8_t rsrq; + uint8_t imei[NRF91_SLM_IMEI_LEN]; + uint8_t model_id[NRF91_SLM_MODEL_ID_LEN]; + uint8_t imsi[NRF91_SLM_IMSI_LEN]; + uint8_t iccid[NRF91_SLM_ICCID_LEN]; + uint8_t manufacturer[NRF91_SLM_MANUFACTURER_LEN]; + uint8_t fw_version[NRF91_SLM_FW_VERSION_LEN]; + + enum nrf91_slm_state state; + const struct device *dev; + struct k_work_delayable timeout_work; + + /* Power management */ + struct k_sem suspended_sem; + + /* Event dispatcher */ + struct k_work event_dispatch_work; + uint8_t event_buf[8]; + struct ring_buf event_rb; + struct k_mutex event_rb_lock; + + /* Network interface */ + struct net_if *netif; + uint8_t mac_addr[6]; + + /* DNS */ + struct zsock_addrinfo dns_result; + struct sockaddr dns_result_addr; + char dns_result_canonname[DNS_MAX_NAME_SIZE + 1]; + + /* Poll */ + struct zsock_pollfd *poll_fds; + int poll_nfds; + int poll_count; + + /* Context for the offloaded socket API. */ + struct modem_socket_config socket_config; + struct modem_socket sockets[1]; +}; + +int nrf91_slm_socket(struct nrf91_slm_data *data, int family, int type, int proto); + +int nrf91_slm_connect(struct nrf91_slm_data *data, void *obj, const struct sockaddr *addr, + socklen_t addrlen); + +ssize_t nrf91_slm_recvfrom(struct nrf91_slm_data *data, void *obj, void *buf, size_t max_len, + int flags, struct sockaddr *src_addr, socklen_t *addrlen); + +ssize_t nrf91_slm_sendto(struct nrf91_slm_data *data, void *obj, const void *buf, size_t len, + int flags, const struct sockaddr *dest_addr, socklen_t addrlen); + +int nrf91_slm_close(struct nrf91_slm_data *data, void *obj); + +int nrf91_slm_poll(struct nrf91_slm_data *data, struct zsock_pollfd *fds, int nfds, int timeout_ms); + +int nrf91_slm_getaddrinfo(struct nrf91_slm_data *data, const char *node, const char *service, + const struct zsock_addrinfo *hints, struct zsock_addrinfo **res); + +void nrf91_slm_freeaddrinfo(struct nrf91_slm_data *data, struct zsock_addrinfo *res); + +#ifdef __cplusplus +} +#endif + +#endif /* MODEM_NORDIC_NRF91_SLM_H_ */ diff --git a/drivers/modem/nordic/nrf91_slm_dns.c b/drivers/modem/nordic/nrf91_slm_dns.c new file mode 100644 index 0000000000000..0525cc8d8ee23 --- /dev/null +++ b/drivers/modem/nordic/nrf91_slm_dns.c @@ -0,0 +1,156 @@ +/* + * Copyright 2025 Nova Dynamics LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nrf91_slm.h" + +#include +LOG_MODULE_DECLARE(nrf91_slm, CONFIG_MODEM_LOG_LEVEL); + +static void nrf91_slm_chat_on_xgetaddrinfo(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + char buf[NET_IPV6_ADDR_LEN + 1]; + char *start; + char *end; + uintptr_t len; + int ret; + + if (argc != 2) { + return; + } + + start = strchr(argv[1], '\"'); + if (!start) { + LOG_ERR("malformed DNS response!!"); + return; + } + + end = strchr(start + 1, '\"'); + if (!end) { + LOG_ERR("malformed DNS response!!"); + return; + } + + len = end - start - 1; + memcpy(buf, start + 1, len); + buf[len] = '\0'; + + ret = net_addr_pton(data->dns_result.ai_family, buf, + &((struct sockaddr_in *)&data->dns_result_addr)->sin_addr); + + if (ret < 0) { + LOG_ERR("failed to convert address (%d)", ret); + memset(&data->dns_result, 0, sizeof(data->dns_result)); + memset(&data->dns_result_addr, 0, sizeof(data->dns_result_addr)); + return; + } +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); +MODEM_CHAT_MATCH_DEFINE(abort_match, "ERROR", "", NULL); +MODEM_CHAT_MATCH_DEFINE(xgetaddrinfo_match, "#XGETADDRINFO: ", "", nrf91_slm_chat_on_xgetaddrinfo); + +/* AT#XGETADDRINFO=[,] */ +static int nrf91_slm_xgetaddrinfo(struct nrf91_slm_data *data, const char *hostname, int family) +{ + struct modem_chat_script script; + struct modem_chat_script_chat script_chats[2]; + char request[sizeof("AT#XGETADDRINF=##,#") + NET_IPV6_ADDR_LEN]; + int ret; + + ret = snprintk(request, sizeof(request), "AT#XGETADDRINFO=\"%s\",%d", hostname, family); + if (ret < 0) { + return ret; + } + + modem_chat_script_chat_init(&script_chats[0]); + modem_chat_script_chat_set_request(&script_chats[0], request); + modem_chat_script_chat_set_response_matches(&script_chats[0], &xgetaddrinfo_match, 1); + + modem_chat_script_chat_init(&script_chats[1]); + modem_chat_script_chat_set_request(&script_chats[1], ""); + modem_chat_script_chat_set_response_matches(&script_chats[1], &ok_match, 1); + + modem_chat_script_init(&script); + modem_chat_script_set_name(&script, "xgetaddrinfo"); + modem_chat_script_set_script_chats(&script, script_chats, 2); + modem_chat_script_set_abort_matches(&script, &abort_match, 1); + modem_chat_script_set_timeout(&script, 120000); + + return modem_chat_run_script(&data->chat, &script); +} + +int nrf91_slm_getaddrinfo(struct nrf91_slm_data *data, const char *node, const char *service, + const struct zsock_addrinfo *hints, struct zsock_addrinfo **res) +{ + uint32_t port = 0; + int ret; + + /* Modem is not attached to the network. */ + if (data->state != NRF91_SLM_STATE_CARRIER_ON) { + LOG_ERR("modem currently not attached to the network!"); + return DNS_EAI_AGAIN; + } + + /* init result */ + memset(&data->dns_result, 0, sizeof(data->dns_result)); + memset(&data->dns_result_addr, 0, sizeof(data->dns_result_addr)); + + /* Currently only support IPv4. */ + data->dns_result.ai_family = AF_INET; + data->dns_result_addr.sa_family = AF_INET; + data->dns_result.ai_addr = &data->dns_result_addr; + data->dns_result.ai_addrlen = sizeof(data->dns_result_addr); + data->dns_result.ai_canonname = data->dns_result_canonname; + data->dns_result_canonname[0] = '\0'; + + if (service) { + port = atoi(service); + if (port < 1 || port > USHRT_MAX) { + return DNS_EAI_SERVICE; + } + } + + if (port > 0 && data->dns_result.ai_family == AF_INET) { + net_sin(&data->dns_result_addr)->sin_port = htons(port); + } + + /* Check if node is an IP address */ + if (net_addr_pton(data->dns_result.ai_family, node, + &((struct sockaddr_in *)&data->dns_result_addr)->sin_addr) == 0) { + *res = &data->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 = k_mutex_lock(&data->chat_lock, K_SECONDS(1)); + if (ret < 0) { + errno = -ret; + return -1; + } + + ret = nrf91_slm_xgetaddrinfo(data, node, AF_INET); + k_mutex_unlock(&data->chat_lock); + + if (ret < 0) { + errno = -ret; + return DNS_EAI_SYSTEM; + } + + *res = (struct zsock_addrinfo *)&data->dns_result; + return 0; +} + +void nrf91_slm_freeaddrinfo(struct nrf91_slm_data *data, struct zsock_addrinfo *res) +{ + ARG_UNUSED(data); + ARG_UNUSED(res); +} diff --git a/drivers/modem/nordic/nrf91_slm_socket.c b/drivers/modem/nordic/nrf91_slm_socket.c new file mode 100644 index 0000000000000..ed80964a166ea --- /dev/null +++ b/drivers/modem/nordic/nrf91_slm_socket.c @@ -0,0 +1,709 @@ +/* + * Copyright 2025 Nova Dynamics LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nrf91_slm.h" + +#include +#include +LOG_MODULE_DECLARE(nrf91_slm, CONFIG_MODEM_LOG_LEVEL); + +struct nrf91_slm_parse_context { + uint8_t *start; + uint8_t *end; + size_t size; + uintptr_t len; +}; + +static void nrf91_slm_parse_xpoll(struct nrf91_slm_data *data, struct nrf91_slm_parse_context *ctx) +{ + struct modem_socket *sock; + int revents; + int id; + + /* + * #XPOLL: %d,"%x"\r\n + * │ │ + * │ └ end + * └ start + */ + id = strtoul((char *)ctx->start + 7, (char **)&ctx->start, 10); + + sock = modem_socket_from_id(&data->socket_config, id); + if (sock == NULL) { + LOG_WRN("invalid socket id (%d)", id); + return; + } + + /* + * #XPOLL: %d,"%x"\r\n + * │ │ + * │ └ end + * └ start + */ + revents = strtoul((char *)ctx->start + 1, NULL, 16); + + for (int i = 0; i < data->poll_nfds; i++) { + if (data->poll_fds[i].fd == sock->sock_fd) { + data->poll_fds[i].revents = revents; + data->poll_count++; + break; + } + } +} + +static void nrf91_slm_parse_xrecv(struct nrf91_slm_data *data, struct nrf91_slm_parse_context *ctx) +{ + /* + * #XRECV: %d\r\n......\r\nOK\r\n + * │ │ + * │ └ end + * └ start + */ + int pending = strtoul((char *)ctx->start + 7, NULL, 10); + + if (ctx->size < pending) { + LOG_WRN("lost %d bytes", pending - ctx->size); + pending = ctx->size; + } + + ctx->start = ctx->end + 1; + ctx->size -= ctx->len + 1; + ctx->end = ctx->start + pending; + ctx->len = pending; + + LOG_HEXDUMP_DBG(ctx->start, ctx->len, "received: "); + + /* + * |-len-| + * #XRECV: %d\r\n......\r\nOK\r\n + * │ │ + * │ └ end + * └ start + */ + ring_buf_put(&data->sock_recv_rb, ctx->start, ctx->len); +} + +static void nrf91_slm_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + struct nrf91_slm_parse_context parse_ctx; + int ret; + + if (event == MODEM_PIPE_EVENT_RECEIVE_READY) { + parse_ctx.start = data->sock_receive_buf; + parse_ctx.size = sizeof(data->sock_receive_buf); + + ret = modem_pipe_receive(pipe, parse_ctx.start, parse_ctx.size); + if (ret < 0) { + LOG_ERR("failed to receive data (%d)", ret); + return; + } + + parse_ctx.size = ret; + parse_ctx.end = memchr(parse_ctx.start, '\n', parse_ctx.size); + + while (parse_ctx.end != NULL) { + parse_ctx.len = parse_ctx.end - parse_ctx.start; + + if (parse_ctx.len > 1) { + /* Print non-blank lines */ + LOG_DBG("%.*s", (int)parse_ctx.len, parse_ctx.start); + } + + if (memcmp(parse_ctx.start, "#XPOLL:", 7) == 0) { + nrf91_slm_parse_xpoll(data, &parse_ctx); + } else if (memcmp(parse_ctx.start, "#XRECV:", 7) == 0) { + nrf91_slm_parse_xrecv(data, &parse_ctx); + } else if (memcmp(parse_ctx.start, "OK", 2) == 0) { + k_sem_give(&data->sock_recv_sem); + break; + } else if (memcmp(parse_ctx.start, "ERROR", 5) == 0) { + k_sem_give(&data->sock_recv_sem); + break; + } + + + /* Next line */ + parse_ctx.start = parse_ctx.end + 1; + parse_ctx.size -= parse_ctx.len + 1; + parse_ctx.end = memchr(parse_ctx.start, '\n', parse_ctx.size); + } + } else if (event == MODEM_PIPE_EVENT_TRANSMIT_IDLE) { + if (data->sock_send_buf_len == 0) { + return; + } + + ret = modem_pipe_transmit(pipe, data->sock_send_buf, + data->sock_send_buf_len); + + if (ret < 0) { + LOG_ERR("error during pipe transmit (%d)", ret); + data->sock_send_buf_len = 0; + } else { + LOG_DBG("transmitted %d bytes", ret); + data->sock_send_count += ret; + data->sock_send_buf += ret; + data->sock_send_buf_len -= ret; + } + + if (data->sock_send_buf_len == 0) { + k_sem_give(&data->sock_send_sem); + } + } +} + +static void nrf91_slm_chat_on_xsocket(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + struct modem_socket *sock = &data->sockets[0]; + + /* TODO: support more than one socket */ + if (argc == 4) { + /* New modem socket created */ + sock->id = atoi(argv[1]); + LOG_INF("socket id %d assigned to fd %d", sock->id, sock->sock_fd); + } else { + /* Active modem socket closed */ + LOG_INF("closed socket"); + } +} + +static void nrf91_slm_chat_on_xconnect(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct nrf91_slm_data *data = (struct nrf91_slm_data *)user_data; + + if (atoi(argv[1]) == 1) { + /* TODO: We only support one socket right now */ + data->sockets[0].is_connected = true; + } +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); +MODEM_CHAT_MATCH_DEFINE(abort_match, "ERROR", "", NULL); +MODEM_CHAT_MATCH_DEFINE(xsocket_match, "#XSOCKET: ", ",", nrf91_slm_chat_on_xsocket); +MODEM_CHAT_MATCH_DEFINE(xconnect_match, "#XCONNECT: ", "", nrf91_slm_chat_on_xconnect); +MODEM_CHAT_MATCH_DEFINE(xdatamode_match, "#XDATAMODE: ", "", NULL); + +/* AT#XSOCKET=[,,] */ +static int nrf91_slm_xsocket(struct nrf91_slm_data *data, int op, int type) +{ + struct modem_chat_script script; + struct modem_chat_script_chat script_chats[2]; + char request[sizeof("AT#XSOCKET=#,#,#")]; + int ret; + + if (op == 0) { + /* Close the active socket */ + strncpy(request, "AT#XSOCKET=0", sizeof(request)); + } else { + /* Open a new socket */ + ret = snprintk(request, sizeof(request), "AT#XSOCKET=%d,%d,0", op, type); + if (ret < 0) { + return ret; + } + } + + modem_chat_script_chat_init(&script_chats[0]); + modem_chat_script_chat_set_request(&script_chats[0], request); + modem_chat_script_chat_set_response_matches(&script_chats[0], &xsocket_match, 1); + + modem_chat_script_chat_init(&script_chats[1]); + modem_chat_script_chat_set_request(&script_chats[1], ""); + modem_chat_script_chat_set_response_matches(&script_chats[1], &ok_match, 1); + modem_chat_script_chat_set_timeout(&script_chats[1], 100); + + modem_chat_script_init(&script); + modem_chat_script_set_name(&script, "xsocket"); + modem_chat_script_set_script_chats(&script, script_chats, 2); + modem_chat_script_set_abort_matches(&script, &abort_match, 1); + modem_chat_script_set_timeout(&script, 10); + + return modem_chat_run_script(&data->chat, &script); +} + +/* AT#XCONNECT=, */ +static int nrf91_slm_xconnect(struct nrf91_slm_data *data, const char *ip_str, uint16_t port) +{ + struct modem_chat_script script; + struct modem_chat_script_chat script_chats[2]; + char request[sizeof("AT#XCONNECT=##,####") + NET_IPV6_ADDR_LEN]; + int ret; + + ret = snprintk(request, sizeof(request), "AT#XCONNECT=\"%s\",%d", ip_str, port); + if (ret < 0) { + return ret; + } + + modem_chat_script_chat_init(&script_chats[0]); + modem_chat_script_chat_set_request(&script_chats[0], request); + modem_chat_script_chat_set_response_matches(&script_chats[0], &xconnect_match, 1); + + modem_chat_script_chat_init(&script_chats[1]); + modem_chat_script_chat_set_request(&script_chats[1], ""); + modem_chat_script_chat_set_response_matches(&script_chats[1], &ok_match, 1); + modem_chat_script_chat_set_timeout(&script_chats[1], 100); + + modem_chat_script_init(&script); + modem_chat_script_set_name(&script, "xconnect"); + modem_chat_script_set_script_chats(&script, script_chats, 2); + modem_chat_script_set_abort_matches(&script, &abort_match, 1); + modem_chat_script_set_timeout(&script, 160); + + return modem_chat_run_script(&data->chat, &script); +} + +/* AT#XSEND */ +static int nrf91_slm_xsend(struct nrf91_slm_data *data, const void *buf, size_t len) +{ + struct modem_chat_script script; + struct modem_chat_script_chat script_chat; + char request[sizeof("AT#XSEND")]; + int ret; + + /* Enter SLM data mode */ + strncpy(request, "AT#XSEND", sizeof(request)); + modem_chat_script_chat_init(&script_chat); + modem_chat_script_chat_set_request(&script_chat, request); + modem_chat_script_chat_set_response_matches(&script_chat, &ok_match, 1); + + modem_chat_script_init(&script); + modem_chat_script_set_name(&script, "xsend"); + modem_chat_script_set_script_chats(&script, &script_chat, 1); + modem_chat_script_set_abort_matches(&script, &abort_match, 1); + modem_chat_script_set_timeout(&script, 31); + + ret = modem_chat_run_script(&data->chat, &script); + if (ret < 0) { + LOG_ERR("failed to enter data mode (%d)", ret); + return ret; + } + + LOG_HEXDUMP_DBG(buf, len, "sending: "); + + data->sock_send_buf = buf; + data->sock_send_buf_len = len; + data->sock_send_count = 0; + + k_sem_reset(&data->sock_send_sem); + + modem_chat_release(&data->chat); + modem_pipe_attach(data->uart_pipe, nrf91_slm_pipe_callback, data); + + /* Wait for transmit */ + ret = k_sem_take(&data->sock_send_sem, K_SECONDS(30)); + if (ret < 0) { + LOG_ERR("failed to take semaphore (%d)", ret); + } + + modem_chat_attach(&data->chat, data->uart_pipe); + + /* Exit SLM data mode */ + /* TODO: The '+++' terminator should be configurable */ + strncpy(request, "+++", sizeof(request)); + modem_chat_script_chat_init(&script_chat); + modem_chat_script_chat_set_request(&script_chat, request); + modem_chat_script_chat_set_response_matches(&script_chat, &xdatamode_match, 1); + + modem_chat_script_init(&script); + modem_chat_script_set_name(&script, "xsend"); + modem_chat_script_set_script_chats(&script, &script_chat, 1); + modem_chat_script_set_abort_matches(&script, &abort_match, 1); + modem_chat_script_set_timeout(&script, 31); + + ret = modem_chat_run_script(&data->chat, &script); + if (ret < 0) { + LOG_ERR("failed to exit data mode (%d)", ret); + } + + return data->sock_send_count; +} + +/* AT#XRECV=[,] */ +static int nrf91_slm_xrecv(struct nrf91_slm_data *data, int timeout_s, int flags) +{ + struct modem_chat *chat = &data->chat; + char request[sizeof("AT#XRECV=####,###") + chat->delimiter_size]; + int ret; + int i; + + __ASSERT(timeout_s >= 0, "Timeout must be >= 0"); + + ret = snprintk(request, sizeof(request) - chat->delimiter_size, "AT#XRECV=%d,%d", timeout_s, + flags); + + if (ret < 0) { + return ret; + } + + LOG_DBG("%.*s", ret, request); + + /* Append the delimiter */ + for (i = 0; i < chat->delimiter_size; i++) { + request[ret++] = chat->delimiter[i]; + } + + data->sock_send_buf = request; + data->sock_send_buf_len = ret; + data->sock_send_count = 0; + + k_sem_reset(&data->sock_recv_sem); + + modem_chat_release(chat); + modem_pipe_attach(data->uart_pipe, nrf91_slm_pipe_callback, data); + + /* Wait for 'OK' */ + ret = k_sem_take(&data->sock_recv_sem, K_SECONDS(timeout_s + 1)); + if (ret < 0) { + LOG_ERR("failed to take semaphore (%d)", ret); + } + + modem_chat_attach(chat, data->uart_pipe); + return ret; +} + +/* AT#XPOLL=[,,...] */ +static int nrf91_slm_xpoll(struct nrf91_slm_data *data, struct zsock_pollfd *fds, int nfds, + int timeout_ms) +{ + char request[64]; + struct modem_chat *chat = &data->chat; + struct modem_socket *sock; + int len; + int ret; + int i; + + __ASSERT(fds != NULL, "fds must not be NULL"); + + data->poll_fds = fds; + data->poll_nfds = nfds; + data->poll_count = 0; + + ret = snprintk(request, sizeof(request), "AT#XPOLL=%d", timeout_ms); + if (ret < 0) { + return ret; + } + + len = ret; + + /* Append the socket ids */ + for (i = 0; i < nfds; i++) { + sock = modem_socket_from_fd(&data->socket_config, fds[i].fd); + + ret = snprintk(request + len, sizeof(request) - len, ",%d", sock->id); + if (ret < 0) { + return ret; + } + + len += ret; + } + + /* Append the delimiter */ + if (len + chat->delimiter_size >= sizeof(request)) { + return -ENOMEM; + } + + LOG_DBG("%.*s", len, request); + + for (i = 0; i < chat->delimiter_size; i++) { + request[len++] = chat->delimiter[i]; + } + + data->sock_send_buf = request; + data->sock_send_buf_len = len; + data->sock_send_count = 0; + + k_sem_reset(&data->sock_recv_sem); + + modem_chat_release(chat); + modem_pipe_attach(data->uart_pipe, nrf91_slm_pipe_callback, data); + + /* Wait for "OK" */ + ret = k_sem_take(&data->sock_recv_sem, K_MSEC(timeout_ms + 500)); + if (ret < 0) { + LOG_ERR("failed to take semaphore (%d)", ret); + } + + modem_chat_attach(chat, data->uart_pipe); + return ret; +} + +int nrf91_slm_socket(struct nrf91_slm_data *data, int family, int type, int proto) +{ + int sock_fd; + int ret; + + ret = modem_socket_get(&data->socket_config, family, type, proto); + if (ret < 0) { + errno = -ret; + return -1; + } + + sock_fd = ret; + + ret = k_mutex_lock(&data->chat_lock, K_SECONDS(10)); + if (ret < 0) { + modem_socket_put(&data->socket_config, sock_fd); + errno = -ret; + return -1; + } + + ret = nrf91_slm_xsocket(data, family, type); + k_mutex_unlock(&data->chat_lock); + + if (ret < 0) { + LOG_ERR("failed to create socket (%d)", ret); + modem_socket_put(&data->socket_config, sock_fd); + errno = -ret; + return -1; + } + + errno = 0; + return sock_fd; +} + +int nrf91_slm_connect(struct nrf91_slm_data *data, void *obj, const struct sockaddr *addr, + socklen_t addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + char ip_str[NET_IPV6_ADDR_LEN]; + void *src = NULL; + uint16_t port = 0; + int ret; + + if (!addr) { + errno = EINVAL; + return -1; + } + + if (data->state != NRF91_SLM_STATE_CARRIER_ON) { + LOG_ERR("modem is not attached to the network!"); + errno = EAGAIN; + return -1; + } + + memcpy(&sock->dst, addr, sizeof(*addr)); + + switch (addr->sa_family) { + case AF_INET6: { + src = &net_sin6(addr)->sin6_addr; + port = ntohs(net_sin6(addr)->sin6_port); + break; + } + case AF_INET: { + src = &net_sin(addr)->sin_addr; + port = ntohs(net_sin(addr)->sin_port); + break; + } + default: + errno = EAFNOSUPPORT; + return -1; + } + + net_addr_ntop(addr->sa_family, src, ip_str, sizeof(ip_str)); + + ret = k_mutex_lock(&data->chat_lock, K_SECONDS(1)); + if (ret < 0) { + errno = -ret; + return -1; + } + + ret = nrf91_slm_xconnect(data, ip_str, port); + k_mutex_unlock(&data->chat_lock); + + if (ret < 0) { + LOG_ERR("failed to connect socket (%d)", ret); + errno = -ret; + return -1; + } + + if (sock->is_connected) { + LOG_INF("socket %d connected to %s:%d", sock->id, ip_str, port); + } + + errno = 0; + return 0; +} + +ssize_t nrf91_slm_recvfrom(struct nrf91_slm_data *data, 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; + int ret = 0; + + if (ring_buf_size_get(&data->sock_recv_rb) < max_len) { + if (data->state != NRF91_SLM_STATE_CARRIER_ON) { + LOG_ERR("modem is not attached to the network!"); + errno = EAGAIN; + return -1; + } + + ret = k_mutex_lock(&data->chat_lock, K_SECONDS(1)); + if (ret < 0) { + errno = -ret; + return -1; + } + + /* Request more data from the modem */ + if (sock->type == SOCK_STREAM) { + ret = nrf91_slm_xrecv(data, 1, flags); + } else { + /* TODO: Add XRECVFROM for SOCK_DGRAM */ + ret = -ESOCKTNOSUPPORT; + } + + k_mutex_unlock(&data->chat_lock); + + if (ret < 0) { + errno = -ret; + return -1; + } + } + + errno = 0; + return ring_buf_get(&data->sock_recv_rb, buf, max_len); +} + +ssize_t nrf91_slm_sendto(struct nrf91_slm_data *data, void *obj, const void *buf, size_t len, + int flags, const struct sockaddr *dest_addr, socklen_t addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + char ip_str[NET_IPV6_ADDR_LEN]; + void *src = NULL; + uint16_t port = 0; + int ret; + + ARG_UNUSED(flags); + + if (data->state != NRF91_SLM_STATE_CARRIER_ON) { + LOG_ERR("modem is not attached to the network!"); + errno = EAGAIN; + return -1; + } + + if (dest_addr != NULL && addrlen > 0) { + if (sock->type == SOCK_STREAM) { + errno = EISCONN; + return -1; + } + + switch (dest_addr->sa_family) { + case AF_INET6: { + src = &net_sin6(dest_addr)->sin6_addr; + port = ntohs(net_sin6(dest_addr)->sin6_port); + break; + } + case AF_INET: { + src = &net_sin(dest_addr)->sin_addr; + port = ntohs(net_sin(dest_addr)->sin_port); + break; + } + default: + errno = EAFNOSUPPORT; + return -1; + } + + net_addr_ntop(dest_addr->sa_family, src, ip_str, sizeof(ip_str)); + } + + ret = k_mutex_lock(&data->chat_lock, K_SECONDS(1)); + if (ret < 0) { + errno = -ret; + return -1; + } + + if (sock->type == SOCK_STREAM) { + ret = nrf91_slm_xsend(data, buf, len); + } else { + /* TODO: Add XSENDTO for SOCK_DGRAM */ + ret = -ESOCKTNOSUPPORT; + } + + k_mutex_unlock(&data->chat_lock); + + if (ret < 0) { + errno = -ret; + return -1; + } + + errno = 0; + return ret; +} + +int nrf91_slm_close(struct nrf91_slm_data *data, void *obj) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + int ret; + + ret = k_mutex_lock(&data->chat_lock, K_SECONDS(1)); + if (ret < 0) { + errno = -ret; + return -1; + } + + ret = nrf91_slm_xsocket(data, 0, 0); + k_mutex_unlock(&data->chat_lock); + + modem_socket_put(&data->socket_config, sock->sock_fd); + + if (ret < 0 && ret != -EAGAIN) { + LOG_WRN("failed to close socket (%d)", ret); + errno = -ret; + return -1; + } + + return 0; +} + +int nrf91_slm_poll(struct nrf91_slm_data *data, struct zsock_pollfd *fds, int nfds, int timeout_ms) +{ + int ret; + int64_t start_ms; + int64_t delta_ms; + k_timeout_t timeout; + + if (data->state != NRF91_SLM_STATE_CARRIER_ON) { + LOG_ERR("modem is not attached to the network!"); + errno = EAGAIN; + return -1; + } + + if (timeout_ms < 0) { + timeout = K_FOREVER; + } else if (timeout_ms == 0) { + timeout = K_NO_WAIT; + } else { + timeout = K_MSEC(timeout_ms); + } + + start_ms = k_uptime_get(); + ret = k_mutex_lock(&data->chat_lock, timeout); + if (ret < 0) { + errno = -ret; + return -1; + } + + delta_ms = k_uptime_delta(&start_ms); + if (timeout_ms >= delta_ms) { + timeout_ms -= delta_ms; + } + + ret = nrf91_slm_xpoll(data, fds, nfds, timeout_ms); + k_mutex_unlock(&data->chat_lock); + + if (ret < 0) { + LOG_ERR("failed to poll sockets (%d)", ret); + errno = -ret; + return -1; + } + + LOG_DBG("poll count: %d", data->poll_count); + + errno = 0; + return data->poll_count; +}