From 064bf94b79169f1c94a64e749d7b4f3ef8ca232c Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Sun, 8 Jun 2025 22:21:03 +0100 Subject: [PATCH 1/7] drivers: modem: HL78XX Modem Driver Adding HL78XX Modem Driver Implementation Using Modem Chat Framework Signed-off-by: Zafer SEN --- drivers/modem/CMakeLists.txt | 2 + drivers/modem/Kconfig | 2 +- drivers/modem/hl78xx/CMakeLists.txt | 18 + drivers/modem/hl78xx/Kconfig.hl78xx | 623 ++++++ drivers/modem/hl78xx/hl78xx.c | 1937 +++++++++++++++++ drivers/modem/hl78xx/hl78xx.h | 558 +++++ drivers/modem/hl78xx/hl78xx_apis.c | 257 +++ .../hl78xx/hl78xx_evt_monitor/CMakeLists.txt | 10 + .../Kconfig.hl78xx_evt_monitor | 29 + .../hl78xx_evt_monitor/hl78xx_evt_monitor.c | 111 + .../hl78xx_evt_monitor/hl78xx_evt_monitor.ld | 5 + drivers/modem/hl78xx/hl78xx_sockets.c | 1624 ++++++++++++++ drivers/modem/hl78xx/hl78xx_utility.c | 491 +++++ include/zephyr/drivers/modem/hl78xx_apis.h | 557 +++++ 14 files changed, 6223 insertions(+), 1 deletion(-) create mode 100644 drivers/modem/hl78xx/CMakeLists.txt create mode 100644 drivers/modem/hl78xx/Kconfig.hl78xx create mode 100644 drivers/modem/hl78xx/hl78xx.c create mode 100644 drivers/modem/hl78xx/hl78xx.h create mode 100644 drivers/modem/hl78xx/hl78xx_apis.c create mode 100644 drivers/modem/hl78xx/hl78xx_evt_monitor/CMakeLists.txt create mode 100644 drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor create mode 100644 drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.c create mode 100644 drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.ld create mode 100644 drivers/modem/hl78xx/hl78xx_sockets.c create mode 100644 drivers/modem/hl78xx/hl78xx_utility.c create mode 100644 include/zephyr/drivers/modem/hl78xx_apis.h diff --git a/drivers/modem/CMakeLists.txt b/drivers/modem/CMakeLists.txt index 62b84bd79294..2e8a0ad90470 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_HL78XX hl78xx) + 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 b8e66ef42748..833bebff3178 100644 --- a/drivers/modem/Kconfig +++ b/drivers/modem/Kconfig @@ -192,7 +192,7 @@ source "drivers/modem/Kconfig.quectel-bg9x" source "drivers/modem/Kconfig.wncm14a2a" source "drivers/modem/Kconfig.cellular" source "drivers/modem/Kconfig.at_shell" - +source "drivers/modem/hl78xx/Kconfig.hl78xx" source "drivers/modem/Kconfig.hl7800" source "drivers/modem/Kconfig.simcom-sim7080" diff --git a/drivers/modem/hl78xx/CMakeLists.txt b/drivers/modem/hl78xx/CMakeLists.txt new file mode 100644 index 000000000000..f8439ea96142 --- /dev/null +++ b/drivers/modem/hl78xx/CMakeLists.txt @@ -0,0 +1,18 @@ +# +# Copyright (c) 2025 Netfeasa Ltd. +# +# SPDX-License-Identifier: Apache-2.0 +# +zephyr_library() + +zephyr_library_sources( + hl78xx.c + hl78xx_sockets.c + hl78xx_utility.c + hl78xx_apis.c +) + +add_subdirectory_ifdef(CONFIG_HL78XX_EVT_MONITOR hl78xx_evt_monitor) + +zephyr_library_include_directories(${ZEPHYR_BASE}/subsys/net/ip) +zephyr_library_include_directories(${ZEPHYR_BASE}/subsys/net/lib/sockets) diff --git a/drivers/modem/hl78xx/Kconfig.hl78xx b/drivers/modem/hl78xx/Kconfig.hl78xx new file mode 100644 index 000000000000..626640f09855 --- /dev/null +++ b/drivers/modem/hl78xx/Kconfig.hl78xx @@ -0,0 +1,623 @@ +# Sierra Wireless HL78XX driver driver options + +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +config MODEM_HL78XX + bool "HL78XX modem driver" + select MODEM_MODULES + select MODEM_CHAT + select MODEM_PIPE + select MODEM_PIPELINK + select MODEM_BACKEND_UART + select RING_BUFFER + select MODEM_SOCKET + select NET_OFFLOAD + select MODEM_CONTEXT + select EXPERIMENTAL + depends on !MODEM_CELLULAR + imply GPIO + help + Choose this setting to enable Sierra Wireless HL78XX driver LTE-CatM1/NB-IoT modem + driver. + +if MODEM_HL78XX + +choice MODEM_HL78XX_VARIANT + bool "Sierra Wireless hl78xx variant selection" + default MODEM_HL7812 + +config MODEM_HL7812 + bool "Sierra Wireless hl7812" + help + Support for hl7812 modem + +config MODEM_HL7800_1 + bool "Sierra Wireless hl7800" + help + Support for hl7800 modem + +config MODEM_HL78XX_AUTODETECT_VARIANT + bool "detect automatically" + help + Automatic detection of modem variant (MODEM_HL7812 or MODEM_HL7800) + +endchoice + +if MODEM_HL7812 + +config MODEM_FW_R6 + bool "Modem firmware R6" + help + Only for HL7812, enable this setting to use NBNTN rat. + This is required for NBNTN mode. + NBNTN mode is supported with R6 firmware. + +endif # MODEM_HL7812 + +config MODEM_HL78XX_UART_BUFFER_SIZES + int "The UART receive and transmit buffer sizes in bytes." + default 512 + +config MODEM_HL78XX_CHAT_BUFFER_SIZES + int "The size of the buffers used for the chat scripts in bytes." + default 512 + +config MODEM_HL78XX_USER_PIPE_BUFFER_SIZES + int "The size of the buffers used for each user pipe in bytes." + default 512 + +config MODEM_HL78XX_NEW_BAUDRATE + int "New baudrate to configure modem to, if supported" + range 9600 4000000 + default 3000000 if DT_HAS_U_BLOX_LARA_R6_ENABLED + default 115200 + +config MODEM_HL78XX_NEW_BAUDRATE_DELAY + int "Time modem takes to change baudrate, in milliseconds" + range 0 1000 + default 100 if DT_HAS_U_BLOX_LARA_R6_ENABLED + default 300 + +config MODEM_HL78XX_RECV_BUF_CNT + int "The number of allocated network buffers" + default 30 + +config MODEM_HL78XX_RECV_BUF_SIZE + int "The size of the network buffers in bytes" + default 128 + +config MODEM_HL78XX_RX_WORKQ_STACK_SIZE + int "Stack size for the Sierra Wireless HL78XX driver modem driver work queue" + default 2048 + help + This stack is used by the work queue to pass off net_pkt data + to the rest of the network stack, letting the rx thread continue + processing data. + +choice MODEM_HL78XX_ADDRESS_FAMILY + prompt "IP Address family" + default MODEM_HL78XX_ADDRESS_FAMILY_IPV4V6 + help + The address family for IP connections. + +config MODEM_HL78XX_ADDRESS_FAMILY_IPV4 + bool "IPv4" + +config MODEM_HL78XX_ADDRESS_FAMILY_IPV6 + bool "IPv6" + +config MODEM_HL78XX_ADDRESS_FAMILY_IPV4V6 + bool "IPv4v6" + +endchoice + +choice MODEM_HL78XX_BOOT_MODE + prompt "Modem Boot Type" + default MODEM_HL78XX_BOOT_IN_AIRPLANE_MODE + help + Set Modem Functionality see, AT+CFUN + Consider reset conditions after settings, second parameter of cfun + 0 — Do not reset the MT before setting it to power level. + 1 — Reset the MT before setting it to power level. + +config MODEM_HL78XX_BOOT_IN_MINIMUM_FUNCTIONAL_MODE + bool "MINIMUM FUNCTIONAL MODE" + help + - AT+CFUN = 0,0 + — Minimum functionality, SIM powered off + - Consider reset conditions second parameter of cfun + +config MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE + bool "FULL FUNCTIONAL MODE" + help + - AT+CFUN = 1,0 + - Full functionality, starts cellular searching + - Consider reset conditions after settings, second parameter of cfun + +config MODEM_HL78XX_BOOT_IN_AIRPLANE_MODE + bool "AIRPLANE MODE" + help + - AT+CFUN = 4,0 + - Disable radio transmit and receive; SIM powered on. (i.e. "Airplane + Mode") + - Consider reset conditions after settings, second parameter of cfun +endchoice + +if MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE + +config MODEM_STAY_IN_BOOT_MODE_FOR_ROAMING + bool "WAIT FOR ROAMING" + help + Keep the device in boot mode until have +CREG/+CEREG: 1(normal) or 5(roaming) +endif # MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE + +config MODEM_HL78XX_PERIODIC_SCRIPT_MS + int "Periodic script interval in milliseconds" + default 2000 + +choice MODEM_HL78XX_APN_SOURCE + prompt "APN SOURCE" + default MODEM_HL78XX_APN_SOURCE_NETWORK + help + Select the source for automatically detecting the APN. + You can choose between IMSI (International Mobile Subscriber Identity) + or ICCID (Integrated Circuit Card Identifier) as the reference for APN association. + +config MODEM_HL78XX_APN_SOURCE_ICCID + bool "CCID Associated APN" + help + - AT+CCID + - Multiple ICCID and APN combinations can be stored in APN PROFILE configuration + see MODEM_HL78XX_APN_PROFILES + +config MODEM_HL78XX_APN_SOURCE_IMSI + bool "CIMI Associated APN" + help + - AT+CIMI + - Multiple CIMI and APN combinations can be stored in APN PROFILE configuration + see MODEM_HL78XX_APN_PROFILES + +config MODEM_HL78XX_APN_SOURCE_KCONFIG + bool "User defined Single APN" + help + - Use the APN defined in MODEM_HL78XX_APN + - Supports only one APN + +config MODEM_HL78XX_APN_SOURCE_NETWORK + bool "Network Provided APN" + help + - AT+CGCONTRDP=1 + - Use the APN provided by the network +endchoice + +if MODEM_HL78XX_APN_SOURCE_KCONFIG + +config MODEM_HL78XX_APN + string "APN for establishing network connection" + default "xxxxxxxx" + help + This setting is used in the AT+CGDCONT command to set the APN name + for the network connection context. This value is specific to + the network provider and has to be changed. + +endif # MODEM_HL78XX_APN_SOURCE_KCONFIG + +if MODEM_HL78XX_APN_SOURCE_ICCID || MODEM_HL78XX_APN_SOURCE_IMSI + +config MODEM_HL78XX_APN_PROFILES + string "list of profiles to search when autodetecting APN" + default "hologram=23450, wm=20601, int=29505" + help + Set a comma separated list of profiles, each containing of: + = ... + = ... + +endif # MODEM_HL78XX_APN_SOURCE_ICCID || MODEM_HL78XX_APN_SOURCE_IMSI + +config MODEM_HL78XX_RSSI_WORK + bool "RSSI polling work" + default y + help + Sierra Wireless HL78XX driver device is configured to poll for RSSI + +config MODEM_HL78XX_RSSI_WORK_PERIOD + int "Configure RSSI WORK polling frequency" + depends on MODEM_HL78XX_RSSI_WORK + default 30 + help + This settings is used to configure the period of RSSI polling + +config MODEM_HL78XX_AUTORAT + bool "automatic RAT switching and set the PRL profiles" + default y + help + AT+KSRAT is provided for backwards compatibility only. AT+KSELACQ is recommended for RAT switching. + (See RAT Switching Application Note (Doc# 2174296) for details.) + +if MODEM_HL78XX_AUTORAT + +config MODEM_HL78XX_AUTORAT_OVER_WRITE_PRL + bool "Overwrite PRL profiles always at boot" + depends on MODEM_HL78XX_AUTORAT + help + If you enable this option, the PRL profiles on the modem will be overwritten by the app + with the PRL profile values at boot everytime. + +config MODEM_HL78XX_AUTORAT_PRL_PROFILES + string "Configure Preferred Radio Access Technology List" + depends on MODEM_HL78XX_AUTORAT + default "1,2,3" + help + AT+KSELACQ=0,1,2,3 , MODEM,CAT-M1, NB-IoT, GSM + +config MODEM_HL78XX_AUTORAT_NB_BAND_CFG + string "NB-IoT band configuration (comma-separated list)" + default "8,20,28" + depends on MODEM_HL78XX_AUTORAT + help + Specify which LTE bands (e.g., 8,20,28) to use for NB-IoT when using Auto RAT. + This string is parsed at runtime or build-time. + +config MODEM_HL78XX_AUTORAT_M1_BAND_CFG + string "Cat-M1 band configuration (comma-separated list)" + default "3,5,12" + depends on MODEM_HL78XX_AUTORAT + help + Specify which LTE bands (e.g., 3,5,12) to use for Cat-M1 when using Auto RAT. + +endif # MODEM_HL78XX_AUTORAT + +choice MODEM_HL78XX_RAT + bool "Radio Access Technology Mode" + default MODEM_HL78XX_RAT_M1 + depends on !MODEM_HL78XX_AUTORAT + +config MODEM_HL78XX_RAT_M1 + bool "LTE-M1" + help + Enable LTE Cat-M1 mode during modem init. + In the Read response, '0' indicates CAT-M1. + +config MODEM_HL78XX_RAT_NB1 + bool "NB-IoT" + help + Enable LTE Cat-NB1 mode during modem init. + 1 — NB-IoT (HL78XX/HL7802/HL7810/HL7845/HL7812 only) + +config MODEM_HL78XX_RAT_GSM + bool "GSM" + depends on MODEM_HL7812 + help + Enable GSM mode during modem init. + 2 — GSM (for HL7802/HL7812 only) + +config MODEM_HL78XX_RAT_NBNTN + bool "NB-NTN" + depends on MODEM_FW_R6 + help + Enable NBNTN mode during modem init. + 3 — NBNTN (for HL7810/HL7812 only), It does not support = 1 + +endchoice + +menuconfig MODEM_HL78XX_CONFIGURE_BANDS + bool "Configure modem bands" + default "y" if !MODEM_HL78XX_AUTORAT + help + Choose this setting to configure which LTE bands the + HL78XX modem should use at boot time. + +if MODEM_HL78XX_CONFIGURE_BANDS + +config MODEM_HL78XX_BAND_1 + bool "Band 1 (2000MHz)" + default y + help + Enable Band 1 (2000MHz) + +config MODEM_HL78XX_BAND_2 + bool "Band 2 (1900MHz)" + default y + help + Enable Band 2 (1900MHz) + +config MODEM_HL78XX_BAND_3 + bool "Band 3 (1800MHz)" + default y + help + Enable Band 3 (1800MHz) + +config MODEM_HL78XX_BAND_4 + bool "Band 4 (1700MHz)" + default y + help + Enable Band 4 (1700MHz) + +config MODEM_HL78XX_BAND_5 + bool "Band 5 (850MHz)" + default y + help + Enable Band 5 (850MHz) + +config MODEM_HL78XX_BAND_8 + bool "Band 8 (900MHz)" + default y + help + Enable Band 8 (900MHz) + +config MODEM_HL78XX_BAND_9 + bool "Band 9 (1900MHz)" + help + Enable Band 9 (1900MHz) + +config MODEM_HL78XX_BAND_10 + bool "Band 10 (2100MHz)" + help + Enable Band 10 (2100MHz) + +config MODEM_HL78XX_BAND_12 + bool "Band 12 (700MHz)" + default y + help + Enable Band 12 (700MHz) + +config MODEM_HL78XX_BAND_13 + bool "Band 13 (700MHz)" + default y + help + Enable Band 13 (700MHz) + +config MODEM_HL78XX_BAND_17 + bool "Band 17 (700MHz)" + help + Enable Band 17 (700MHz) + +config MODEM_HL78XX_BAND_18 + bool "Band 18 (800MHz)" + help + Enable Band 18 (800MHz) + +config MODEM_HL78XX_BAND_19 + bool "Band 19 (800MHz)" + help + Enable Band 19 (800MHz) + +config MODEM_HL78XX_BAND_20 + bool "Band 20 (800MHz)" + default y + help + Enable Band 20 (800MHz) + +config MODEM_HL78XX_BAND_23 + bool "Band 23 (2000MHz)" + help + Enable Band 23 (2000MHz) + +config MODEM_HL78XX_BAND_25 + bool "Band 25 (1900MHz)" + help + Enable Band 25 (1900MHz) + +config MODEM_HL78XX_BAND_26 + bool "Band 26 (800MHz)" + help + Enable Band 26 (800MHz) + +config MODEM_HL78XX_BAND_27 + bool "Band 27 (800MHz)" + help + Enable Band 27 (800MHz) + +config MODEM_HL78XX_BAND_28 + bool "Band 28 (700MHz)" + default y + help + Enable Band 28 (700MHz) + +config MODEM_HL78XX_BAND_31 + bool "Band 31 (450MHz)" + help + Enable Band 31 (450MHz) + +config MODEM_HL78XX_BAND_66 + bool "Band 66 (1800MHz)" + help + Enable Band 66 (1800MHz) + +config MODEM_HL78XX_BAND_72 + bool "Band 72 (450MHz)" + help + Enable Band 72 (450MHz) + +config MODEM_HL78XX_BAND_73 + bool "Band 73 (450MHz)" + help + Enable Band 73 (450MHz) + +config MODEM_HL78XX_BAND_85 + bool "Band 85 (700MHz)" + help + Enable Band 85 (700MHz) + +config MODEM_HL78XX_BAND_87 + bool "Band 87 (410MHz)" + help + Enable Band 87 (410MHz) + +config MODEM_HL78XX_BAND_88 + bool "Band 88 (410MHz)" + help + Enable Band 88 (410MHz) + +config MODEM_HL78XX_BAND_106 + bool "Band 106 (900MHz)" + help + Enable Band 106 (900MHz) + +config MODEM_HL78XX_BAND_107 + bool "Band 107 (1800MHz)" + help + Enable Band 107 (1800MHz) + +config MODEM_HL78XX_BAND_255 + bool "Band 255 (1500MHz)" + help + Enable Band 255 (1500MHz) + +config MODEM_HL78XX_BAND_256 + bool "Band 256 (2000MHz)" + help + Enable Band 256 (2000MHz) + +endif # MODEM_HL78XX_CONFIGURE_BAND + +config MODEM_HL78XX_LOW_POWER_MODE + bool "Low power modes" + help + Choose this setting to enable a low power mode for the HL78XX modem + +if MODEM_HL78XX_LOW_POWER_MODE + +config MODEM_HL78XX_EDRX + bool "eDRX" + depends on MODEM_HL78XX_LOW_POWER_MODE + help + Enable LTE eDRX + +config MODEM_HL78XX_PSM + bool "PSM" + depends on MODEM_HL78XX_LOW_POWER_MODE + default y + help + Enable Power Save Mode (PSM) + +if MODEM_HL78XX_EDRX + +config MODEM_HL78XX_EDRX_VALUE + string "Requested eDRX timer" + default "0101" + help + Half a byte in a 4-bit format. The eDRX value refers to bit 4 to 1 + of octet 3 of the Extended DRX parameters information element. + Default value is 81.92 seconds. + +endif # MODEM_HL78XX_EDRX + +if MODEM_HL78XX_PSM + +config MODEM_HL78XX_PSM_PERIODIC_TAU + string "Requested extended periodic TAU timer" + default "10000010" + help + Requested extended periodic TAU (tracking area update) value (T3412) + to be allocated to the UE in E-UTRAN. One byte in an 8-bit format. + Default value is 1 minute. + +config MODEM_HL78XX_PSM_ACTIVE_TIME + string "Requested active time" + default "00001111" + help + Requested Active Time value (T3324) to be allocated to the UE. + One byte in an 8-bit format. Default value is 30 seconds. + +endif # MODEM_HL78XX_PSM + +choice MODEM_DEFAULT_SLEEP_LEVEL + prompt "Default Sleep Level" + default MODEM_HL78XX_SLEEP_LEVEL_HIBERNATE + help + The application can override this setting + +config MODEM_HL78XX_SLEEP_LEVEL_HIBERNATE + bool "Hibernate" + help + Lowest power consumption + IO state not retained + Application subsystem OFF + +config MODEM_HL78XX_SLEEP_LEVEL_LITE_HIBERNATE + bool "Lite Hibernate" + help + IO state retained + Application subsystem OFF + +config MODEM_HL78XX_SLEEP_LEVEL_SLEEP + bool "Sleep" + help + Highest power consumption of modem sleep states + IO state retained + Application subsystem ON + Allows sockets to remain open + +endchoice + +config MODEM_HL78XX_SLEEP_DELAY_AFTER_REBOOT + int "Delay in seconds before sleep after reboot" + default 10 + +endif # MODEM_HL78XX_LOW_POWER_MODE + +config MODEM_HL78XX_ADVANCED_SOCKET_CONFIG + bool "Advanced socket configuration" + help + Enable advanced socket configuration options + +if MODEM_HL78XX_ADVANCED_SOCKET_CONFIG + +config MODEM_HL78XX_NUM_SOCKETS + int "Maximum number of sockets" + default 6 + range 1 6 + help + Maximum number of sockets that can be opened at the same time + +config MODEM_HL78XX_SOCKET_UDP_DISPLAY_DATA_URC + int "display data in URC" + default 2 + help + 0 — Do not display data in URC + 1 — Display data in URC automatically + 2 — Do not display data in URC and KUDPRCV command is required to dump + data. If there is no KUDPRCV command after rcv_timeout, the original data is + dropped and URC re-enabled. + +config MODEM_HL78XX_SOCKET_RESTORE_ON_BOOT + bool "Restore sockets on boot" + help + only the first session is restored + For HL780x, restore_on_boot is required to restore the first session across + eDRX/PSM hibernate cycles or reset. + • For HL781x/45, all sessions are maintained across eDRX/PSM hibernate cycles + independent of this configuration. It is only required for reset cases. + • For a restored client session (e.g. after a reset or exiting hibernation), +KTCPCNX + must be used to establish a connection before sending/receiving any data. + 0 — Do not restore sockets on boot + 1 — Restore sockets on boot + +endif # MODEM_HL78XX_ADVANCED_SOCKET_CONFIG + +config MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG +bool "Verbose debug output in the HL78xx" + help + Enabling this setting will turn on VERY heavy debugging from the + modem. Do NOT leave on for production. + +config MODEM_HL78XX_DEV_INIT_PRIORITY + int "Sierra Wireless HL78XX device driver init priority" + default 80 + help + Sierra Wireless HL78XX device driver initialization priority. + Do not mess with it unless you know what you are doing. + +config MODEM_HL78XX_OFFLOAD_INIT_PRIORITY + int "Sierra Wireless HL78XX offload driver init priority" + default 79 + help + Sierra Wireless HL78XX driver device driver initialization priority. + Do not mess with it unless you know what you are doing. + Make sure offload init priority higher than dev init priority + +rsource "hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor" + +endif # MODEM_HL78XX diff --git a/drivers/modem/hl78xx/hl78xx.c b/drivers/modem/hl78xx/hl78xx.c new file mode 100644 index 000000000000..7862b93d8b0c --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx.c @@ -0,0 +1,1937 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "hl78xx.h" + +#define MAX_SCRIPT_AT_CMD_RETRY 3 + +#define MDM_NODE DT_ALIAS(modem) + +/* GPIO availability macros */ +#define HAS_PWR_ON_GPIO DT_NODE_HAS_PROP(MDM_NODE, mdm_pwr_on_gpios) +#define HAS_FAST_SHUTD_GPIO DT_NODE_HAS_PROP(MDM_NODE, mdm_fast_shutd_gpios) +#define HAS_UART_DTR_GPIO DT_NODE_HAS_PROP(MDM_NODE, mdm_uart_dtr_gpios) +#define HAS_GPIO6_GPIO DT_NODE_HAS_PROP(MDM_NODE, mdm_gpio6_gpios) +#define HAS_UART_DSR_GPIO DT_NODE_HAS_PROP(MDM_NODE, mdm_uart_dsr_gpios) + +/* GPIO count macro */ +#define GPIO_CONFIG_LEN \ + (3 /* reset, wake, vgpio */ + HAS_PWR_ON_GPIO + HAS_FAST_SHUTD_GPIO + HAS_UART_DTR_GPIO + \ + HAS_GPIO6_GPIO + HAS_UART_DSR_GPIO) + +LOG_MODULE_REGISTER(hl78xx_dev, CONFIG_MODEM_LOG_LEVEL); + +/* RX thread work queue */ +K_KERNEL_STACK_DEFINE(modem_workq_stack, CONFIG_MODEM_HL78XX_RX_WORKQ_STACK_SIZE); + +static struct k_work_q modem_workq; +hl78xx_evt_monitor_handler_t event_dispatcher; + +static void hl78xx_event_handler(struct hl78xx_data *data, enum hl78xx_event evt); +static int hl78xx_on_idle_state_enter(struct hl78xx_data *data); +static void hl78xx_begin_power_off_pulse(struct hl78xx_data *data); + +static void event_dispatcher_dispatch(struct hl78xx_evt *notif) +{ + if (event_dispatcher != NULL) { + event_dispatcher(notif); + } +} + +static const char *hl78xx_state_str(enum hl78xx_state state) +{ + switch (state) { + case MODEM_HL78XX_STATE_IDLE: + return "idle"; + case MODEM_HL78XX_STATE_RESET_PULSE: + return "reset pulse"; + case MODEM_HL78XX_STATE_POWER_ON_PULSE: + return "power pulse"; + case MODEM_HL78XX_STATE_AWAIT_POWER_ON: + return "await power on"; + case MODEM_HL78XX_STATE_SET_BAUDRATE: + return "set baudrate"; + case MODEM_HL78XX_STATE_RUN_INIT_SCRIPT: + return "run init script"; + case MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT: + return "init fail diagnostic script "; + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: + return "await registered"; + case MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT: + return "run rat cfg script"; + case MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT: + return "run enable gprs script"; + case MODEM_HL78XX_STATE_CARRIER_ON: + return "carrier on"; + case MODEM_HL78XX_STATE_CARRIER_OFF: + return "carrier on"; + case MODEM_HL78XX_STATE_INIT_POWER_OFF: + return "init power off"; + case MODEM_HL78XX_STATE_POWER_OFF_PULSE: + return "power off pulse"; + case MODEM_HL78XX_STATE_SIM_POWER_OFF: + return "sim power off"; + case MODEM_HL78XX_STATE_AIRPLANE: + return "airplane mode"; + case MODEM_HL78XX_STATE_AWAIT_POWER_OFF: + return "await power off"; + default: + return "UNKNOWN state"; + } + + return ""; +} + +static const char *hl78xx_event_str(enum hl78xx_event event) +{ + switch (event) { + case MODEM_HL78XX_EVENT_RESUME: + return "resume"; + case MODEM_HL78XX_EVENT_SUSPEND: + return "suspend"; + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + return "script success"; + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + return "script failed"; + case MODEM_HL78XX_EVENT_SCRIPT_REQUIRE_RESTART: + return "script require restart"; + case MODEM_HL78XX_EVENT_TIMEOUT: + return "timeout"; + case MODEM_HL78XX_EVENT_REGISTERED: + return "registered"; + case MODEM_HL78XX_EVENT_DEREGISTERED: + return "deregistered"; + case MODEM_HL78XX_EVENT_BUS_OPENED: + return "bus opened"; + case MODEM_HL78XX_EVENT_BUS_CLOSED: + return "bus closed"; + case MODEM_HL78XX_EVENT_SOCKET_READY: + return "socket ready"; + default: + return "unknown event"; + } + + return ""; +} + +static bool hl78xx_gpio_is_enabled(const struct gpio_dt_spec *gpio) +{ + return (gpio->port != NULL); +} + +static void hl78xx_log_event(enum hl78xx_event evt) +{ + LOG_DBG("event %s", hl78xx_event_str(evt)); +} + +static void hl78xx_start_timer(struct hl78xx_data *data, k_timeout_t timeout) +{ + k_work_schedule(&data->timeout_work, timeout); +} + +static void hl78xx_stop_timer(struct hl78xx_data *data) +{ + k_work_cancel_delayable(&data->timeout_work); +} + +static void hl78xx_timeout_handler(struct k_work *item) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(item); + struct hl78xx_data *data = CONTAINER_OF(dwork, struct hl78xx_data, timeout_work); + + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_TIMEOUT); +} + +static void hl78xx_bus_pipe_handler(struct modem_pipe *pipe, enum modem_pipe_event event, + void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + switch (event) { + case MODEM_PIPE_EVENT_OPENED: + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_BUS_OPENED); + break; + + case MODEM_PIPE_EVENT_CLOSED: + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_BUS_CLOSED); + break; + + default: + break; + } +} + +static void hl78xx_log_state_changed(enum hl78xx_state last_state, enum hl78xx_state new_state) +{ + LOG_DBG("switch from %s to %s", hl78xx_state_str(last_state), hl78xx_state_str(new_state)); +} + +static void hl78xx_event_dispatch_handler(struct k_work *item) +{ + struct hl78xx_data *data = + CONTAINER_OF(item, struct hl78xx_data, events.event_dispatch_work); + + uint8_t events[sizeof(data->events.event_buf)]; + uint8_t events_cnt; + + k_mutex_lock(&data->events.event_rb_lock, K_FOREVER); + + events_cnt = (uint8_t)ring_buf_get(&data->events.event_rb, events, + sizeof(data->events.event_buf)); + + k_mutex_unlock(&data->events.event_rb_lock); + + for (uint8_t i = 0; i < events_cnt; i++) { + hl78xx_event_handler(data, (enum hl78xx_event)events[i]); + } +} + +void hl78xx_delegate_event(struct hl78xx_data *data, enum hl78xx_event evt) +{ + k_mutex_lock(&data->events.event_rb_lock, K_FOREVER); + ring_buf_put(&data->events.event_rb, (uint8_t *)&evt, 1); + k_mutex_unlock(&data->events.event_rb_lock); + k_work_submit_to_queue(&modem_workq, &data->events.event_dispatch_work); +} + +static void hl78xx_chat_callback_handler(struct modem_chat *chat, + enum modem_chat_script_result result, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (result == MODEM_CHAT_SCRIPT_RESULT_SUCCESS) { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SCRIPT_SUCCESS); + } else { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SCRIPT_FAILED); + } +} + +static void hl78xx_on_cxreg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + enum hl78xx_registration_status registration_status = 0; + + if (argc >= 2) { + /* +CEREG: [,[...]] */ + registration_status = atoi(argv[1]); + } else { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %d %s:%d", __LINE__, __func__, argc, argv[0], registration_status); +#endif + data->status.registration.network_state = registration_status; + struct hl78xx_evt event = {.type = HL78XX_LTE_REGISTRATION_STAT_UPDATE, + .content.reg_status = data->status.registration.network_state}; + + event_dispatcher_dispatch(&event); + + if (hl78xx_is_registered(data)) { + data->status.registration.is_registered = true; + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_REGISTERED); + } else { + data->status.registration.is_registered = false; + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_DEREGISTERED); + } +} + +static void hl78xx_on_psmev(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + + LOG_DBG("%d %s %d", __LINE__, __func__, argc); + if (argc >= 2) { + /* +CEREG: [,[...]] */ + LOG_DBG("%d %s %s", __LINE__, __func__, argv[1]); + } else { + LOG_DBG("%d %s %s", __LINE__, __func__, argv[0]); + } +#endif +} + +static void hl78xx_on_ksup(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + char ksup_data[2] = {0}; + + strncpy(ksup_data, argv[1], sizeof(ksup_data) - 1); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("Module status: %s", ksup_data); +#endif +} + +static void hl78xx_on_imei(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc != 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("IMEI: %s %s", argv[0], argv[1]); +#endif + strncpy(data->identity.imei, argv[1], sizeof(data->identity.imei) - 1); +} + +static void hl78xx_on_cgmm(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc != 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("cgmm: %s %s", argv[0], argv[1]); +#endif + strncpy(data->identity.model_id, argv[1], sizeof(data->identity.model_id) - 1); +} + +static void hl78xx_on_imsi(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc != 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("IMSI: %s %s", argv[0], argv[1]); +#endif + strncpy(data->identity.imsi, argv[1], sizeof(data->identity.imsi) - 1); +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI) + /* set the APN automatically */ + modem_detect_apn(data, argv[1]); +#endif +} + +static void hl78xx_on_cgmi(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc != 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("cgmi: %s %s", argv[0], argv[1]); +#endif + strncpy(data->identity.manufacturer, argv[1], sizeof(data->identity.manufacturer) - 1); +} + +static void hl78xx_on_cgmr(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc != 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("cgmr: %s %s", argv[0], argv[1]); +#endif + strncpy(data->identity.fw_version, argv[1], sizeof(data->identity.fw_version) - 1); +} + +static void hl78xx_on_iccid(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc != 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("ICCID: %s %s", argv[0], argv[1]); +#endif + strncpy(data->identity.iccid, argv[1], sizeof(data->identity.iccid) - 1); +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_ICCID) + /* set the APN automatically */ + modem_detect_apn(data, argv[1]); +#endif +} + +/* Handler: +KSTATEV: */ +static void hl78xx_on_kstatev(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + enum hl78xx_cell_rat_mode rat_mode = HL78XX_RAT_MODE_NONE; + + if (argc != 3) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("KSTATEV: %s %s %s", argv[0], argv[1], argv[2]); +#endif + rat_mode = ATOI(argv[2], 0, "rat_mode"); + + hl78xx_on_kstatev_parser(data, ATOI(argv[1], 0, "status")); + if (rat_mode != data->status.registration.rat_mode) { + struct hl78xx_evt event = {.type = HL78XX_RAT_UPDATE, + .content.rat_mode = data->status.registration.rat_mode}; + + event_dispatcher_dispatch(&event); + } + data->status.registration.rat_mode = rat_mode; +} + +static void hl78xx_on_udprcv(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); +#endif +} + +static void hl78xx_on_socknotifydata(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + int socket_id = -1; + int new_total = -1; + + if (argc < 2) { + return; + } + socket_id = ATOI(argv[1], 0, "socket_id"); + new_total = ATOI(argv[2], 0, "length"); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %d %d", __LINE__, __func__, socket_id, new_total); +#endif + socknotifydata(socket_id, new_total); +} + +static void hl78xx_on_ksrep(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc < 2) { + return; + } + + data->status.ksrep = (uint8_t)atoi(argv[1]); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("KSREP: %s %s", argv[0], argv[1]); +#endif +} +#ifndef CONFIG_MODEM_HL78XX_AUTORAT +static void hl78xx_on_ksrat(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc < 2) { + return; + } + + data->status.registration.rat_mode = (uint8_t)atoi(argv[1]); + + struct hl78xx_evt event = {.type = HL78XX_RAT_UPDATE, + .content.rat_mode = data->status.registration.rat_mode}; + + event_dispatcher_dispatch(&event); + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("KSRAT: %s %s", argv[0], argv[1]); +#endif +} +#endif /* MODEM_HL78XX_RAT */ + +static void hl78xx_on_kselacq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } + + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc > 3) { + data->kselacq_data.mode = 0; + data->kselacq_data.rat1 = ATOI(argv[1], 0, "rat1"); + data->kselacq_data.rat2 = ATOI(argv[2], 0, "rat2"); + data->kselacq_data.rat3 = ATOI(argv[3], 0, "rat3"); + } else { + data->kselacq_data.mode = 0; + data->kselacq_data.rat1 = 0; + data->kselacq_data.rat2 = 0; + data->kselacq_data.rat3 = 0; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%d] [%d] [%d] [%d]", __LINE__, argc, argv[0], data->kselacq_data.mode, + data->kselacq_data.rat1, data->kselacq_data.rat2, data->kselacq_data.rat3); +#endif +} + +static void hl78xx_on_kbndcfg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 3) { + return; + } + + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); +#endif + + uint8_t rat_id = ATOI(argv[1], 0, "rat"); + uint8_t kbnd_bitmap_size = strlen(argv[2]); + + if (kbnd_bitmap_size >= MDM_BAND_HEX_STR_LEN) { + LOG_ERR("%d %s Unexpected band bitmap length of %d", __LINE__, __func__, + kbnd_bitmap_size); + return; + } + if (rat_id >= HL78XX_RAT_COUNT) { + return; + } + data->status.kbndcfg[rat_id].rat = rat_id; + strncpy(data->status.kbndcfg[rat_id].bnd_bitmap, argv[2], kbnd_bitmap_size); + data->status.kbndcfg[rat_id].bnd_bitmap[kbnd_bitmap_size] = '\0'; +} + +static void hl78xx_on_csq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 3) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); +#endif + + data->status.rssi = ATOI(argv[1], 0, "rssi"); +} + +static void hl78xx_on_cesq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 7) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); +#endif + + data->status.rsrq = ATOI(argv[5], 0, "rssi"); + data->status.rsrp = ATOI(argv[6], 0, "rssi"); +} + +static void hl78xx_on_cfun(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] ", __LINE__, argc, argv[0], argv[1]); +#endif + + data->status.phone_functionality = ATOI(argv[1], 0, "phone_func"); +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); +MODEM_CHAT_MATCHES_DEFINE(allow_match, MODEM_CHAT_MATCH("OK", "", NULL), + MODEM_CHAT_MATCH("+CME ERROR: ", "", NULL)); + +MODEM_CHAT_MATCHES_DEFINE(unsol_matches, MODEM_CHAT_MATCH("+CREG: ", ",", hl78xx_on_cxreg), + MODEM_CHAT_MATCH("+CEREG: ", ",", hl78xx_on_cxreg), + MODEM_CHAT_MATCH("+CGREG: ", ",", hl78xx_on_cxreg), + MODEM_CHAT_MATCH("+PSMEV: ", "", hl78xx_on_psmev), + MODEM_CHAT_MATCH("+KSTATEV: ", ",", hl78xx_on_kstatev), + MODEM_CHAT_MATCH("+KUDP_DATA: ", ",", hl78xx_on_socknotifydata), + MODEM_CHAT_MATCH("+KTCP_DATA: ", ",", hl78xx_on_socknotifydata), + MODEM_CHAT_MATCH("+KUDP_RCV: ", ",", hl78xx_on_udprcv), + MODEM_CHAT_MATCH("+KBNDCFG: ", ",", hl78xx_on_kbndcfg), + MODEM_CHAT_MATCH("+CSQ: ", ",", hl78xx_on_csq), + MODEM_CHAT_MATCH("+CESQ: ", ",", hl78xx_on_cesq), + MODEM_CHAT_MATCH("+CFUN: ", "", hl78xx_on_cfun)); + +MODEM_CHAT_MATCHES_DEFINE(abort_matches, MODEM_CHAT_MATCH("ERROR", "", NULL)); +MODEM_CHAT_MATCH_DEFINE(at_ready_match, "+KSUP: ", "", hl78xx_on_ksup); +MODEM_CHAT_MATCH_DEFINE(imei_match, "", "", hl78xx_on_imei); +MODEM_CHAT_MATCH_DEFINE(cgmm_match, "", "", hl78xx_on_cgmm); +MODEM_CHAT_MATCH_DEFINE(cimi_match, "", "", hl78xx_on_imsi); +MODEM_CHAT_MATCH_DEFINE(cgmi_match, "", "", hl78xx_on_cgmi); +MODEM_CHAT_MATCH_DEFINE(cgmr_match, "", "", hl78xx_on_cgmr); +MODEM_CHAT_MATCH_DEFINE(iccid_match, "+CCID: ", "", hl78xx_on_iccid); +MODEM_CHAT_MATCH_DEFINE(ksrep_match, "+KSREP: ", ",", hl78xx_on_ksrep); +#ifndef CONFIG_MODEM_HL78XX_AUTORAT +MODEM_CHAT_MATCH_DEFINE(ksrat_match, "+KSRAT: ", "", hl78xx_on_ksrat); +#endif /* CONFIG_MODEM_HL78XX_RAT */ +MODEM_CHAT_MATCH_DEFINE(kselacq_match, "+KSELACQ: ", ",", hl78xx_on_kselacq); + +static void hl78xx_init_pipe(const struct device *dev) +{ + const struct hl78xx_config *cfg = dev->config; + struct hl78xx_data *data = dev->data; + + const struct modem_backend_uart_config uart_backend_config = { + .uart = cfg->uart, + .receive_buf = data->buffers.uart_rx, + .receive_buf_size = sizeof(data->buffers.uart_rx), + .transmit_buf = data->buffers.uart_tx, + .transmit_buf_size = ARRAY_SIZE(data->buffers.uart_tx), + }; + + data->uart_pipe = modem_backend_uart_init(&data->uart_backend, &uart_backend_config); +} + +static int modem_init_chat(const struct device *dev) +{ + struct hl78xx_data *data = dev->data; + + const struct modem_chat_config chat_config = { + .user_data = data, + .receive_buf = data->buffers.chat_rx, + .receive_buf_size = sizeof(data->buffers.chat_rx), + .delimiter = data->buffers.delimiter, + .delimiter_size = strlen(data->buffers.delimiter), + .filter = data->buffers.filter, + .filter_size = data->buffers.filter ? strlen(data->buffers.filter) : 0, + .argv = data->buffers.argv, + .argv_size = ARRAY_SIZE(data->buffers.argv), + .unsol_matches = unsol_matches, + .unsol_matches_size = ARRAY_SIZE(unsol_matches), + }; + + return modem_chat_init(&data->chat, &chat_config); +} + +MODEM_CHAT_SCRIPT_CMDS_DEFINE( + swir_hl78xx_init_chat_script_cmds, MODEM_CHAT_SCRIPT_CMD_RESP("", at_ready_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KHWIOCFG=3,1,6", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("ATE0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP_MULT("AT+CGACT=0", allow_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CFUN=4", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSLEEP=2", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPSMS=0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEDRXS=0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KPATTERN=" + "\"" EOF_PATTERN "\"", + ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CCID", iccid_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CMEE=1", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+GNSSCONF=10,1", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+GNSSNMEA=0,1000,0,4F", 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_CMD_RESP("AT+CIMI", cimi_match), MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSTATEV=1", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGEREP=2", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSELACQ?", kselacq_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KBNDCFG?", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGACT?", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CREG=0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG=5", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(swir_hl78xx_init_chat_script, swir_hl78xx_init_chat_script_cmds, + abort_matches, hl78xx_chat_callback_handler, 10); + +int modem_cmd_send_int(struct hl78xx_data *data, modem_chat_script_callback script_user_callback, + const uint8_t *cmd, uint16_t cmd_size, + const struct modem_chat_match *response_matches, uint16_t matches_size, + bool user_cmd) +{ + int ret = 0; + + ret = k_mutex_lock(&data->tx_lock, K_NO_WAIT); + if (ret < 0) { + if (user_cmd == false) { + errno = -ret; + } + return -1; + } + /* Optional logic: only set capture mode for CONNECT-type commands */ + struct modem_chat_script_chat dynamic_script = { + .request = cmd, + .request_size = cmd_size, + .response_matches = response_matches, + .response_matches_size = matches_size, + .timeout = 1000, + }; + struct modem_chat_script chat_script = {.name = "dynamic_script", + .script_chats = &dynamic_script, + .script_chats_size = 1, + .abort_matches = abort_matches, + .abort_matches_size = ARRAY_SIZE(abort_matches), + .callback = script_user_callback, + .timeout = 1000}; + + ret = modem_chat_run_script(&data->chat, &chat_script); + if (ret < 0) { + LOG_ERR("%d %s Failed to run at command: %d", __LINE__, __func__, ret); + } else { + LOG_DBG("Chat script executed successfully."); + } + ret = k_mutex_unlock(&data->tx_lock); + if (ret < 0) { + if (user_cmd == false) { + errno = -ret; + } + return -1; + } + return ret; +} + +void mdm_vgpio_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) +{ + ARG_UNUSED(port); + ARG_UNUSED(cb); + ARG_UNUSED(pins); + + const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + + LOG_DBG("VGPIO ISR callback %d", gpio_pin_get_dt(&spec)); +} + +#if DT_INST_NODE_HAS_PROP(0, mdm_uart_dsr_gpios) +void mdm_uart_dsr_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) +{ + ARG_UNUSED(port); + ARG_UNUSED(cb); + ARG_UNUSED(pins); + + const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + + LOG_DBG("DSR ISR callback %d", gpio_pin_get_dt(&spec)); +} +#endif + +void mdm_gpio6_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) +{ + ARG_UNUSED(port); + ARG_UNUSED(cb); + ARG_UNUSED(pins); + + const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + + LOG_DBG("GPIO6 ISR callback %d", gpio_pin_get_dt(&spec)); +} + +void mdm_uart_cts_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) +{ + ARG_UNUSED(port); + ARG_UNUSED(cb); + ARG_UNUSED(pins); + + const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + + LOG_DBG("CTS ISR callback %d", gpio_pin_get_dt(&spec)); +} + +static int hl78xx_on_reset_pulse_state_enter(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { + gpio_pin_set_dt(&config->mdm_gpio_wake, 0); + } + + gpio_pin_set_dt(&config->mdm_gpio_reset, 1); + hl78xx_start_timer(data, K_MSEC(config->reset_pulse_duration_ms)); + return 0; +} + +static void hl78xx_reset_pulse_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_TIMEOUT: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_POWER_ON); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + + default: + break; + } +} + +static int hl78xx_on_reset_pulse_state_leave(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + gpio_pin_set_dt(&config->mdm_gpio_reset, 0); + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { + gpio_pin_set_dt(&config->mdm_gpio_wake, 1); + } + hl78xx_stop_timer(data); + return 0; +} + +static int hl78xx_on_power_on_pulse_state_enter(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + gpio_pin_set_dt(&config->mdm_gpio_pwr_on, 1); + } + hl78xx_start_timer(data, K_MSEC(config->power_pulse_duration_ms)); + return 0; +} + +static void hl78xx_power_on_pulse_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_TIMEOUT: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_POWER_ON); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + + default: + break; + } +} + +static int hl78xx_on_power_on_pulse_state_leave(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + gpio_pin_set_dt(&config->mdm_gpio_pwr_on, 0); + } + hl78xx_stop_timer(data); + return 0; +} + +static int hl78xx_on_await_power_on_state_enter(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + hl78xx_start_timer(data, K_MSEC(config->startup_time_ms)); + return 0; +} + +static void hl78xx_await_power_on_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_TIMEOUT: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_INIT_SCRIPT); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + + default: + break; + } +} +static int hl78xx_on_run_init_script_state_enter(struct hl78xx_data *data) +{ + modem_pipe_attach(data->uart_pipe, hl78xx_bus_pipe_handler, data); + return modem_pipe_open_async(data->uart_pipe); +} + +static void hl78xx_run_init_script_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + switch (evt) { + case MODEM_HL78XX_EVENT_BUS_OPENED: + modem_chat_attach(&data->chat, data->uart_pipe); + modem_chat_run_script_async(&data->chat, config->init_chat_script); + break; + + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT); + + break; + + case MODEM_HL78XX_EVENT_BUS_CLOSED: + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT); + break; + + default: + break; + } +} + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_fail_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP?", ksrep_match)); + +MODEM_CHAT_SCRIPT_DEFINE(init_fail_script, init_fail_script_cmds, abort_matches, + hl78xx_chat_callback_handler, 10); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(swir_hl78xx_enable_ksup_urc_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP=1", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP?", ksrep_match)); + +MODEM_CHAT_SCRIPT_DEFINE(swir_hl78xx_enable_ksup_urc_script, swir_hl78xx_enable_ksup_urc_cmds, + abort_matches, hl78xx_chat_callback_handler, 4); + +static int hl78xx_on_run_init_diagnose_script_state_enter(struct hl78xx_data *data) +{ + modem_chat_run_script_async(&data->chat, &init_fail_script); + return 0; +} +static void hl78xx_run_init_fail_script_event_handler(struct hl78xx_data *data, + enum hl78xx_event evt) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + if (data->status.ksrep == 0) { + modem_chat_run_script_async(&data->chat, + &swir_hl78xx_enable_ksup_urc_script); + hl78xx_start_timer(data, K_MSEC(config->shutdown_time_ms)); + } else { + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RESET_PULSE); + } + } + break; + case MODEM_HL78XX_EVENT_TIMEOUT: + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_POWER_ON_PULSE); + break; + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RESET_PULSE); + break; + } + + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + case MODEM_HL78XX_EVENT_BUS_CLOSED: + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + if (!hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { + LOG_ERR("modem wake pin is not enabled, make sure modem low power is " + "disabled, if you are not sure enable wake up pin by adding it " + "dts!!"); + } + + if (data->status.script_fail_counter++ < MAX_SCRIPT_AT_CMD_RETRY) { + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_POWER_ON_PULSE); + break; + } + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RESET_PULSE); + break; + } + } + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + default: + break; + } +} + +static int hl78xx_rat_cfg(struct hl78xx_data *data, bool *modem_require_restart, + enum hl78xx_cell_rat_mode *rat_request) +{ + int ret = 0; +#if defined(CONFIG_MODEM_HL78XX_AUTORAT) + /* Check autorat status/configs */ + if (IS_ENABLED(CONFIG_MODEM_HL78XX_AUTORAT_OVER_WRITE_PRL) || + (data->kselacq_data.rat1 == 0 && data->kselacq_data.rat2 == 0 && + data->kselacq_data.rat3 == 0)) { + char cmd_kselq[] = "AT+KSELACQ=0," CONFIG_MODEM_HL78XX_AUTORAT_PRL_PROFILES; + /* Re-congfiguring PRL context definition */ + ret = modem_cmd_send_int(data, NULL, cmd_kselq, strlen(cmd_kselq), &ok_match, 1, + false); + if (ret < 0) { + goto error; + } else { + *modem_require_restart = true; + } + } + + *rat_request = HL78XX_RAT_MODE_AUTO; +#else + /* Check a active rat config */ + if (data->kselacq_data.rat1 != 0 && data->kselacq_data.rat2 != 0 && + data->kselacq_data.rat3 != 0) { + char const *cmd_kselq_disable = (const char *)DISABLE_RAT_AUTO; + + /* Re-congfiguring PRL context definition */ + ret = modem_cmd_send_int(data, NULL, cmd_kselq_disable, strlen(cmd_kselq_disable), + &ok_match, 1, false); + if (ret < 0) { + goto error; + } + } + + char const *cmd_ksrat_query = (const char *)KSRAT_QUERY; + + /* Re-congfiguring PRL context definition */ + ret = modem_cmd_send_int(data, NULL, cmd_ksrat_query, strlen(cmd_ksrat_query), &ksrat_match, + 1, false); + if (ret < 0) { + goto error; + } +#if !defined(CONFIG_MODEM_HL78XX_RAT_M1) && !defined(CONFIG_MODEM_HL78XX_RAT_NB1) && \ + !defined(CONFIG_MODEM_HL78XX_RAT_GSM) && !defined(CONFIG_MODEM_HL78XX_RAT_NBNTN) +#error "No rat has been selected." +#endif + const char *cmd_set_rat = NULL; + + if (IS_ENABLED(CONFIG_MODEM_HL78XX_RAT_M1)) { + cmd_set_rat = (const char *)SET_RAT_M1_CMD_LEGACY; + + *rat_request = HL78XX_RAT_CAT_M1; + } else if (IS_ENABLED(CONFIG_MODEM_HL78XX_RAT_NB1)) { + cmd_set_rat = (const char *)SET_RAT_NB1_CMD_LEGACY; + + *rat_request = HL78XX_RAT_NB1; + } +#ifdef CONFIG_MODEM_HL7812 + else if (IS_ENABLED(CONFIG_MODEM_HL78XX_RAT_GSM)) { + + cmd_set_rat = (const char *)SET_RAT_GSM_CMD_LEGACY; + + *rat_request = HL78XX_RAT_GSM; + } +#ifdef CONFIG_MODEM_FW_R6 + else if (IS_ENABLED(CONFIG_MODEM_HL78XX_RAT_NBNTN)) { + cmd_set_rat = (const char *)SET_RAT_NBNTN_CMD_LEGACY; + + *rat_request = HL78XX_RAT_NBNTN; + } +#endif /* CONFIG_MODEM_FW_R6 */ +#endif + else { + LOG_ERR("%d %s No rat has been selected.", __LINE__, __func__); + } + + if (cmd_set_rat == NULL || *rat_request == HL78XX_RAT_MODE_NONE) { + ret = -EINVAL; + goto error; + } + + if (*rat_request != data->status.registration.rat_mode) { + ret = modem_cmd_send_int(data, NULL, cmd_set_rat, strlen(cmd_set_rat), &ok_match, 1, + false); + if (ret < 0) { + goto error; + } else { + *modem_require_restart = true; + } + } + +#endif /* CONFIG_MODEM_HL78XX_AUTORAT */ +error: + return ret; +} + +static int hl78xx_band_cfg(struct hl78xx_data *data, bool *modem_require_restart, + enum hl78xx_cell_rat_mode rat_config_request) +{ + int ret = 0; + char bnd_bitmap[MDM_BAND_HEX_STR_LEN] = {0}; + + if (rat_config_request == HL78XX_RAT_MODE_NONE) { + return -EINVAL; + } +#ifdef CONFIG_MODEM_HL78XX_AUTORAT + /* In Auto-RAT mode, configure both M1 and NB-IoT band configs */ + for (int rat = HL78XX_RAT_CAT_M1; rat <= HL78XX_RAT_NB1; rat++) { + if (rat == HL78XX_RAT_GSM) { + continue; /* skip unsupported RAT for band config */ + } +#else + /* Otherwise, just configure for the requested RAT */ + int rat = rat_config_request; +#endif + ret = hl78xx_get_band_default_config_for_rat(rat, bnd_bitmap, + ARRAY_SIZE(bnd_bitmap)); + if (ret) { + LOG_ERR("%d %s error get band default config %d", __LINE__, __func__, ret); + goto error; + } + const char *modem_trimmed = + hl78xx_trim_leading_zeros(data->status.kbndcfg[rat].bnd_bitmap); + const char *expected_trimmed = hl78xx_trim_leading_zeros(bnd_bitmap); + + if (strcmp(modem_trimmed, expected_trimmed) != 0) { + char cmd_bnd[80] = {0}; + + snprintf(cmd_bnd, sizeof(cmd_bnd), "AT+KBNDCFG=%d,%s", rat, + bnd_bitmap); /* RAT=0 for CAT-M1 */ + ret = modem_cmd_send_int(data, NULL, cmd_bnd, strlen(cmd_bnd), &ok_match, 1, + false); + if (ret < 0) { + goto error; + } else { + *modem_require_restart |= true; + } + } else { + LOG_DBG("The band configs (%s) matched with exist configs (%s) for rat: " + "[%d]", + modem_trimmed, expected_trimmed, rat); + } +#ifdef CONFIG_MODEM_HL78XX_AUTORAT + } +#endif +error: + return ret; +} + +static int hl78xx_on_rat_cfg_script_state_enter(struct hl78xx_data *data) +{ + int ret = 0; + bool modem_require_restart = false; + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + enum hl78xx_cell_rat_mode rat_config_request = HL78XX_RAT_MODE_NONE; + + ret = hl78xx_rat_cfg(data, &modem_require_restart, &rat_config_request); + if (ret < 0) { + goto error; + } + + ret = hl78xx_band_cfg(data, &modem_require_restart, rat_config_request); + if (ret < 0) { + goto error; + } + + if (modem_require_restart) { + const char *cmd_restart = (const char *)SET_AIRPLANE_MODE_CMD; + + ret = modem_cmd_send_int(data, NULL, cmd_restart, strlen(cmd_restart), &ok_match, 1, + false); + if (ret < 0) { + goto error; + } + hl78xx_start_timer(data, K_MSEC(config->shutdown_time_ms)); + return 0; + } + hl78xx_chat_callback_handler(&data->chat, MODEM_CHAT_SCRIPT_RESULT_SUCCESS, data); + return 0; +error: + hl78xx_chat_callback_handler(&data->chat, MODEM_CHAT_SCRIPT_RESULT_ABORT, data); + LOG_ERR("%d %s Failed to send command: %d", __LINE__, __func__, ret); + return ret; +} + +static void hl78xx_run_rat_cfg_script_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + int ret = 0; + char const *cmd_ksrat_query = (const char *)KSRAT_QUERY; + + switch (evt) { + case MODEM_HL78XX_EVENT_TIMEOUT: + LOG_DBG("Rebooting modem to apply new RAT settings"); + ret = modem_cmd_send_int(data, NULL, NULL, 0, &at_ready_match, 1, false); + if (ret < 0) { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SUSPEND); + } + + /* Re-check if rat config is correct */ + ret = modem_cmd_send_int(data, NULL, cmd_ksrat_query, strlen(cmd_ksrat_query), + &ksrat_match, 1, false); + if (ret < 0) { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SUSPEND); + } + + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SCRIPT_SUCCESS); + break; + + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + default: + break; + } +} + +static int hl78xx_on_run_rat_cfg_script_state_leave(struct hl78xx_data *data) +{ + return 0; +} + +static int hl78xx_on_await_power_off_state_enter(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + hl78xx_start_timer(data, K_MSEC(config->shutdown_time_ms)); + return 0; +} + +static void hl78xx_await_power_off_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + if (evt == MODEM_HL78XX_EVENT_TIMEOUT) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + } +} + +static int hl78xx_on_enable_gprs_state_enter(struct hl78xx_data *data) +{ + int ret = 0; + +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_KCONFIG) + snprintf(data->identity.apn, sizeof(data->identity.apn), "%s", CONFIG_MODEM_HL78XX_APN); +#elif defined(CONFIG_MODEM_HL78XX_APN_SOURCE_ICCID) || defined(CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI) + /* autodetect APN from IMSI */ + /* the list of SIM profiles. Global scope, so the app can change it */ + /* AT+CCID or AT+CIMI needs to be run here if it is not ran in the init script */ + if (strlen(data->identity.apn) < 1) { + LOG_WRN("%d %s APN is left blank", __LINE__, __func__); + } +#else /* defined(CONFIG_MODEM_HL78XX_APN_SOURCE_NETWORK) */ +/* set blank string to get apn from network */ +#endif + ret = hl78xx_api_func_set_phone_functionality(data->dev, HL78XX_AIRPLANE, false); + if (ret) { + goto error; + } + + ret = hl78xx_set_apn_internal(data, data->identity.apn, strlen(data->identity.apn)); + if (ret) { + goto error; + } +#if defined(CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE) + ret = hl78xx_api_func_set_phone_functionality(data->dev, HL78XX_FULLY_FUNCTIONAL, false); + if (ret) { + goto error; + } +#endif /* CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE */ + + hl78xx_chat_callback_handler(&data->chat, MODEM_CHAT_SCRIPT_RESULT_SUCCESS, data); + + return 0; +error: + hl78xx_chat_callback_handler(&data->chat, MODEM_CHAT_SCRIPT_RESULT_ABORT, data); + LOG_ERR("%d %s Failed to send command: %d", __LINE__, __func__, ret); + return ret; +} + +static void hl78xx_enable_gprs_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + hl78xx_start_timer(data, MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT); + break; + + case MODEM_HL78XX_EVENT_TIMEOUT: + break; + + case MODEM_HL78XX_EVENT_REGISTERED: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_CARRIER_ON); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + + default: + break; + } +} + +static int hl78xx_on_await_registered_state_enter(struct hl78xx_data *data) +{ + return 0; +} + +static void hl78xx_await_registered_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + hl78xx_start_timer(data, MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT); + break; + + case MODEM_HL78XX_EVENT_TIMEOUT: + break; + + case MODEM_HL78XX_EVENT_REGISTERED: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_CARRIER_ON); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + + default: + break; + } +} + +static int hl78xx_on_await_registered_state_leave(struct hl78xx_data *data) +{ + hl78xx_stop_timer(data); + return 0; +} + +static int hl78xx_on_carrier_on_state_enter(struct hl78xx_data *data) +{ + iface_status_work_cb(data, hl78xx_chat_callback_handler); + return 0; +} + +static void hl78xx_carrier_on_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + hl78xx_start_timer(data, K_SECONDS(2)); + break; + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + break; + case MODEM_HL78XX_EVENT_TIMEOUT: + dns_work_cb(); + break; + case MODEM_HL78XX_EVENT_DEREGISTERED: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_REGISTERED); + break; + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + + default: + break; + } +} + +static int hl78xx_on_carrier_on_state_leave(struct hl78xx_data *data) +{ + hl78xx_stop_timer(data); + return 0; +} + +static int hl78xx_on_carrier_off_state_enter(struct hl78xx_data *data) +{ + notif_carrier_off(); + /* Check whether or not there is any sockets are connected, + * if true, wait until sockets are closed properly + */ + if (check_if_any_socket_connected() == false) { + hl78xx_start_timer(data, K_MSEC(100)); + } + return 0; +} + +static void hl78xx_carrier_off_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + case MODEM_HL78XX_EVENT_TIMEOUT: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT); + break; + case MODEM_HL78XX_EVENT_DEREGISTERED: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_REGISTERED); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + + default: + break; + } +} + +static int hl78xx_on_carrier_off_state_leave(struct hl78xx_data *data) +{ + hl78xx_stop_timer(data); + return 0; +} + +static int hl78xx_on_init_power_off_state_enter(struct hl78xx_data *data) +{ + hl78xx_start_timer(data, K_MSEC(2000)); + return 0; +} + +static void hl78xx_init_power_off_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + if (evt == MODEM_HL78XX_EVENT_TIMEOUT) { + hl78xx_begin_power_off_pulse(data); + } +} + +static int hl78xx_on_init_power_off_state_leave(struct hl78xx_data *data) +{ + modem_chat_release(&data->chat); + return 0; +} + +static int hl78xx_on_power_off_pulse_state_enter(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + gpio_pin_set_dt(&config->mdm_gpio_pwr_on, 1); + } + hl78xx_start_timer(data, K_MSEC(config->power_pulse_duration_ms)); + return 0; +} + +static void hl78xx_power_off_pulse_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + if (evt == MODEM_HL78XX_EVENT_TIMEOUT) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_POWER_OFF); + } +} + +static int hl78xx_on_power_off_pulse_state_leave(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + gpio_pin_set_dt(&config->mdm_gpio_pwr_on, 0); + } + hl78xx_stop_timer(data); + return 0; +} + +static int hl78xx_on_idle_state_enter(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { + gpio_pin_set_dt(&config->mdm_gpio_wake, 0); + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + gpio_pin_set_dt(&config->mdm_gpio_reset, 1); + } + modem_chat_release(&data->chat); + modem_pipe_close_async(data->uart_pipe); + k_sem_give(&data->suspended_sem); + return 0; +} + +static void hl78xx_idle_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + switch (evt) { + case MODEM_HL78XX_EVENT_RESUME: + if (config->autostarts) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_POWER_ON); + break; + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_POWER_ON_PULSE); + break; + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_POWER_ON); + break; + } + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_INIT_SCRIPT); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + k_sem_give(&data->suspended_sem); + break; + + default: + break; + } +} + +static int hl78xx_on_idle_state_leave(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + k_sem_take(&data->suspended_sem, K_NO_WAIT); + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + gpio_pin_set_dt(&config->mdm_gpio_reset, 0); + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { + gpio_pin_set_dt(&config->mdm_gpio_wake, 1); + } + + return 0; +} + +static int hl78xx_on_state_enter(struct hl78xx_data *data) +{ + int ret = 0; + + switch (data->status.state) { + case MODEM_HL78XX_STATE_IDLE: + ret = hl78xx_on_idle_state_enter(data); + break; + + case MODEM_HL78XX_STATE_RESET_PULSE: + ret = hl78xx_on_reset_pulse_state_enter(data); + break; + + case MODEM_HL78XX_STATE_POWER_ON_PULSE: + ret = hl78xx_on_power_on_pulse_state_enter(data); + break; + + case MODEM_HL78XX_STATE_AWAIT_POWER_ON: + ret = hl78xx_on_await_power_on_state_enter(data); + break; + + case MODEM_HL78XX_STATE_SET_BAUDRATE: + break; + + case MODEM_HL78XX_STATE_RUN_INIT_SCRIPT: + ret = hl78xx_on_run_init_script_state_enter(data); + break; + case MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT: + ret = hl78xx_on_run_init_diagnose_script_state_enter(data); + break; + + case MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT: + ret = hl78xx_on_rat_cfg_script_state_enter(data); + break; + + case MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT: + ret = hl78xx_on_enable_gprs_state_enter(data); + break; + + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: + ret = hl78xx_on_await_registered_state_enter(data); + break; + + case MODEM_HL78XX_STATE_CARRIER_ON: + ret = hl78xx_on_carrier_on_state_enter(data); + break; + + case MODEM_HL78XX_STATE_CARRIER_OFF: + ret = hl78xx_on_carrier_off_state_enter(data); + break; + + case MODEM_HL78XX_STATE_INIT_POWER_OFF: + ret = hl78xx_on_init_power_off_state_enter(data); + break; + + case MODEM_HL78XX_STATE_POWER_OFF_PULSE: + ret = hl78xx_on_power_off_pulse_state_enter(data); + break; + + case MODEM_HL78XX_STATE_AWAIT_POWER_OFF: + ret = hl78xx_on_await_power_off_state_enter(data); + break; + + default: + ret = 0; + break; + } + + return ret; +} + +static int hl78xx_on_state_leave(struct hl78xx_data *data) +{ + int ret = 0; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %d", __LINE__, __func__, data->status.state); +#endif + switch (data->status.state) { + case MODEM_HL78XX_STATE_IDLE: + ret = hl78xx_on_idle_state_leave(data); + break; + + case MODEM_HL78XX_STATE_RESET_PULSE: + ret = hl78xx_on_reset_pulse_state_leave(data); + break; + + case MODEM_HL78XX_STATE_POWER_ON_PULSE: + ret = hl78xx_on_power_on_pulse_state_leave(data); + break; + + case MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT: + ret = hl78xx_on_run_rat_cfg_script_state_leave(data); + break; + + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: + ret = hl78xx_on_await_registered_state_leave(data); + break; + + case MODEM_HL78XX_STATE_CARRIER_ON: + ret = hl78xx_on_carrier_on_state_leave(data); + break; + + case MODEM_HL78XX_STATE_CARRIER_OFF: + ret = hl78xx_on_carrier_off_state_leave(data); + break; + + case MODEM_HL78XX_STATE_INIT_POWER_OFF: + ret = hl78xx_on_init_power_off_state_leave(data); + break; + + case MODEM_HL78XX_STATE_POWER_OFF_PULSE: + ret = hl78xx_on_power_off_pulse_state_leave(data); + break; + + default: + ret = 0; + break; + } + + return ret; +} + +void hl78xx_enter_state(struct hl78xx_data *data, enum hl78xx_state state) +{ + int ret; + + ret = hl78xx_on_state_leave(data); + + if (ret < 0) { + LOG_WRN("failed to leave state, error: %i", ret); + + return; + } + + data->status.state = state; + ret = hl78xx_on_state_enter(data); + + if (ret < 0) { + LOG_WRN("failed to enter state error: %i", ret); + } +} + +static void hl78xx_begin_power_off_pulse(struct hl78xx_data *data) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + modem_pipe_close_async(data->uart_pipe); + + hl78xx_enter_state(data, hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on) + ? MODEM_HL78XX_STATE_POWER_OFF_PULSE + : MODEM_HL78XX_STATE_IDLE); +} + +static void hl78xx_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + enum hl78xx_state state; + + state = data->status.state; + + hl78xx_log_event(evt); + + switch (data->status.state) { + case MODEM_HL78XX_STATE_IDLE: + hl78xx_idle_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_RESET_PULSE: + hl78xx_reset_pulse_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_POWER_ON_PULSE: + hl78xx_power_on_pulse_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_AWAIT_POWER_ON: + hl78xx_await_power_on_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_SET_BAUDRATE: + break; + + case MODEM_HL78XX_STATE_RUN_INIT_SCRIPT: + hl78xx_run_init_script_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT: + hl78xx_run_init_fail_script_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT: + hl78xx_run_rat_cfg_script_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT: + hl78xx_enable_gprs_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: + hl78xx_await_registered_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_CARRIER_ON: + hl78xx_carrier_on_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_CARRIER_OFF: + hl78xx_carrier_off_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_INIT_POWER_OFF: + hl78xx_init_power_off_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_POWER_OFF_PULSE: + hl78xx_power_off_pulse_event_handler(data, evt); + break; + + case MODEM_HL78XX_STATE_AWAIT_POWER_OFF: + hl78xx_await_power_off_event_handler(data, evt); + break; + default: + LOG_ERR("%d %s unknown event", __LINE__, __func__); + break; + } + + if (state != data->status.state) { + hl78xx_log_state_changed(state, data->status.state); + } +} + +#ifdef CONFIG_PM_DEVICE +static int hl78xx_driver_pm_action(const struct device *dev, enum pm_device_action action) +{ + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + int ret = 0; + + LOG_WRN("%d %s PM_DEVICE_ACTION: %d", __LINE__, __func__, action); + switch (action) { + case PM_DEVICE_ACTION_SUSPEND: + /* suspend the device */ + LOG_DBG("%d %s PM_DEVICE_ACTION_SUSPEND", __LINE__, __func__); + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SUSPEND); + ret = k_sem_take(&data->suspended_sem, K_SECONDS(30)); + break; + case PM_DEVICE_ACTION_RESUME: + LOG_DBG("%d %s PM_DEVICE_ACTION_RESUME", __LINE__, __func__); + /* resume the device */ + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_RESUME); + break; + case PM_DEVICE_ACTION_TURN_ON: + /* + * powered on the device, used when the power + * domain this device belongs is resumed. + */ + LOG_DBG("%d %s PM_DEVICE_ACTION_TURN_ON", __LINE__, __func__); + break; + case PM_DEVICE_ACTION_TURN_OFF: + /* + * power off the device, used when the power + * domain this device belongs is suspended. + */ + LOG_DBG("%d %s PM_DEVICE_ACTION_TURN_OFF", __LINE__, __func__); + break; + default: + return -ENOTSUP; + } + return ret; +} +#endif /* CONFIG_PM_DEVICE */ + +static int hl78xx_init(const struct device *dev) +{ + int ret; + const struct hl78xx_config *config = (const struct hl78xx_config *)dev->config; + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + data->dev = dev; +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + /* Initialize work queue and event handling */ + k_work_queue_start(&modem_workq, modem_workq_stack, + K_KERNEL_STACK_SIZEOF(modem_workq_stack), K_PRIO_COOP(7), NULL); + k_work_init_delayable(&data->timeout_work, hl78xx_timeout_handler); + k_work_init(&data->events.event_dispatch_work, hl78xx_event_dispatch_handler); + ring_buf_init(&data->events.event_rb, sizeof(data->events.event_buf), + data->events.event_buf); + k_sem_init(&data->suspended_sem, 0, 1); + k_sem_init(&data->script_stopped_sem_tx_int, 0, 1); + k_sem_init(&data->script_stopped_sem_rx_int, 0, 1); + /* reset to default */ + data->buffers.eof_pattern_size = strlen(data->buffers.eof_pattern); + memset(data->identity.apn, 0, MDM_APN_MAX_LENGTH); + + /* GPIO validation */ + const struct gpio_dt_spec *gpio_pins[GPIO_CONFIG_LEN] = { + &config->mdm_gpio_reset, &config->mdm_gpio_wake, &config->mdm_gpio_vgpio, +#if HAS_GPIO6_GPIO + &config->mdm_gpio_gpio6, +#endif +#if HAS_PWR_ON_GPIO + &config->mdm_gpio_pwr_on, +#endif +#if HAS_FAST_SHUTD_GPIO + &config->mdm_gpio_fast_shtdown, +#endif +#if HAS_UART_DTR_GPIO + &config->mdm_gpio_uart_dtr, +#endif + }; + + for (int i = 0; i < ARRAY_SIZE(gpio_pins); i++) { + if (!gpio_is_ready_dt(gpio_pins[i])) { + LOG_ERR("GPIO port (%s) not ready!", gpio_pins[i]->port->name); + return -ENODEV; + } + } + + /* GPIO configuration */ + struct { + const struct gpio_dt_spec *spec; + gpio_flags_t flags; + const char *name; + } gpio_config[GPIO_CONFIG_LEN] = { + {&config->mdm_gpio_reset, GPIO_OUTPUT_LOW, "reset"}, + {&config->mdm_gpio_wake, GPIO_OUTPUT_HIGH, "wake"}, + {&config->mdm_gpio_vgpio, GPIO_INPUT, "VGPIO"}, +#if HAS_PWR_ON_GPIO + {&config->mdm_gpio_pwr_on, GPIO_OUTPUT_HIGH, "pwr_on"}, +#endif +#if HAS_FAST_SHUTD_GPIO + {&config->mdm_gpio_fast_shtdown, GPIO_OUTPUT_LOW, "fast_shutdown"}, +#endif +#if HAS_UART_DTR_GPIO + {&config->mdm_gpio_uart_dtr, GPIO_OUTPUT, "DTR"}, +#endif +#if HAS_GPIO6_GPIO + {&config->mdm_gpio_gpio6, GPIO_OUTPUT, "GPIO6"}, +#endif + }; + + for (int i = 0; i < ARRAY_SIZE(gpio_config); i++) { + ret = gpio_pin_configure_dt(gpio_config[i].spec, gpio_config[i].flags); + if (ret < 0) { + LOG_ERR("Failed to configure %s pin", gpio_config[i].name); + goto error; + } + } + + /* VGPIO interrupt setup */ + gpio_init_callback(&data->gpio_cbs.vgpio_cb, mdm_vgpio_callback_isr, + BIT(config->mdm_gpio_vgpio.pin)); + + ret = gpio_add_callback(config->mdm_gpio_vgpio.port, &data->gpio_cbs.vgpio_cb); + if (ret) { + LOG_ERR("Cannot setup VGPIO callback! (%d)", ret); + goto error; + } + + ret = gpio_pin_interrupt_configure_dt(&config->mdm_gpio_vgpio, GPIO_INT_EDGE_BOTH); + if (ret) { + LOG_ERR("Error configuring VGPIO interrupt! (%d)", ret); + goto error; + } + + (void)hl78xx_init_pipe(dev); + + ret = modem_init_chat(dev); + if (ret < 0) { + goto error; + } + + hl78xx_socket_init(data); + +#ifndef CONFIG_PM_DEVICE + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_RESUME); +#else + pm_device_init_suspended(dev); +#endif /* CONFIG_PM_DEVICE */ + + return 0; + +error: + return ret; +} + +int hl78xx_evt_notif_handler_set(hl78xx_evt_monitor_handler_t handler) +{ + event_dispatcher = handler; + return 0; +} + +static DEVICE_API(hl78xx, hl78xx_api) = { + + .get_signal = hl78xx_api_func_get_signal, + .get_registration_status = hl78xx_api_func_get_registration_status, + .get_modem_info = hl78xx_api_func_get_modem_info, + .set_apn = hl78xx_api_func_set_apn, + .set_phone_functionality = hl78xx_api_func_set_phone_functionality, + .get_phone_functionality = hl78xx_api_func_get_phone_functionality, + .send_at_cmd = hl78xx_api_func_modem_cmd_send_int, +}; + +#define MODEM_HL78XX_DEFINE_INSTANCE(inst, power_ms, reset_ms, startup_ms, shutdown_ms, start, \ + init_script) \ + static const struct hl78xx_config hl78xx_cfg_##inst = { \ + .uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \ + .mdm_gpio_reset = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_reset_gpios, {}), \ + .mdm_gpio_wake = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_wake_gpios, {}), \ + .mdm_gpio_pwr_on = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_pwr_on_gpios, {}), \ + .mdm_gpio_fast_shtdown = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_fast_shutd_gpios, {}), \ + .mdm_gpio_uart_dtr = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_uart_dtr_gpios, {}), \ + .mdm_gpio_uart_dsr = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_uart_dsr_gpios, {}), \ + .mdm_gpio_uart_cts = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_uart_cts_gpios, {}), \ + .mdm_gpio_vgpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_vgpio_gpios, {}), \ + .mdm_gpio_gpio6 = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_gpio6_gpios, {}), \ + .mdm_gpio_gpio8 = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_gpio8_gpios, {}), \ + .mdm_gpio_sim_switch = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_sim_select_gpios, {}), \ + .power_pulse_duration_ms = (power_ms), \ + .reset_pulse_duration_ms = (reset_ms), \ + .startup_time_ms = (startup_ms), \ + .shutdown_time_ms = (shutdown_ms), \ + .autostarts = (start), \ + .init_chat_script = (init_script), \ + }; \ + static struct hl78xx_data hl78xx_data_##inst = { \ + .buffers.delimiter = "\r\n", \ + .buffers.eof_pattern = EOF_PATTERN, \ + }; \ + PM_DEVICE_DT_INST_DEFINE(inst, hl78xx_driver_pm_action); \ + DEVICE_DT_INST_DEFINE(inst, hl78xx_init, PM_DEVICE_DT_INST_GET(inst), &hl78xx_data_##inst, \ + &hl78xx_cfg_##inst, POST_KERNEL, \ + CONFIG_MODEM_HL78XX_DEV_INIT_PRIORITY, &hl78xx_api); + +#define MODEM_DEVICE_SWIR_HL78XX(inst) \ + MODEM_HL78XX_DEFINE_INSTANCE(inst, 1500, 100, 1000, 1000, false, \ + &swir_hl78xx_init_chat_script) + +#define DT_DRV_COMPAT swir_hl7812 +DT_INST_FOREACH_STATUS_OKAY(MODEM_DEVICE_SWIR_HL78XX) +#undef DT_DRV_COMPAT + +#define DT_DRV_COMPAT swir_hl7800 +DT_INST_FOREACH_STATUS_OKAY(MODEM_DEVICE_SWIR_HL78XX) +#undef DT_DRV_COMPAT diff --git a/drivers/modem/hl78xx/hl78xx.h b/drivers/modem/hl78xx/hl78xx.h new file mode 100644 index 000000000000..2464d00a26aa --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx.h @@ -0,0 +1,558 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef HL78XX_H +#define HL78XX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../modem_context.h" +#include "../modem_socket.h" +#include + +#define MDM_CMD_TIMEOUT (10) /*K_SECONDS*/ +#define MDM_DNS_TIMEOUT (70) /*K_SECONDS*/ +#define MDM_CELL_BAND_SEARCH_TIMEOUT (60) /*K_SECONDS*/ +#define MDM_CMD_CONN_TIMEOUT (120) /*K_SECONDS*/ +#define MDM_REGISTRATION_TIMEOUT (180) /*K_SECONDS*/ +#define MDM_PROMPT_CMD_DELAY (50) /*K_MSEC*/ +#define MDM_RESET_LOW_TIME (1) /*K_MSEC*/ +#define MDM_RESET_HIGH_TIME (10) /*K_MSEC*/ +#define MDM_BOOT_TIME (12) /*K_SECONDS*/ +#define MDM_DNS_ADD_TIMEOUT (100) /*K_MSEC*/ +#define MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT K_MSEC(CONFIG_MODEM_HL78XX_PERIODIC_SCRIPT_MS) + +#define MDM_MAX_DATA_LENGTH CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES + +#define MDM_MAX_SOCKETS 6 +#define MDM_BASE_SOCKET_NUM 1 +#define MDM_BAND_BITMAP_LEN_BYTES 32 +#define MDM_BAND_HEX_STR_LEN (MDM_BAND_BITMAP_LEN_BYTES * 2 + 1) + +#define MDM_KBND_BITMAP_MAX_ARRAY_SIZE 64 + +#define MDM_MANUFACTURER_LENGTH 20 +#define MDM_MODEL_LENGTH 16 +#define MDM_REVISION_LENGTH 64 +#define MDM_IMEI_LENGTH 16 +#define MDM_IMSI_LENGTH 23 +#define MDM_ICCID_LENGTH 22 +#define MDM_APN_MAX_LENGTH 64 + +#define ADDRESS_FAMILY_IP "IP" +#define ADDRESS_FAMILY_IP4 "IPV4" +#define ADDRESS_FAMILY_IPV6 "IPV6" +#define ADDRESS_FAMILY_IPV4V6 "IPV4V6" +#define MDM_HL78XX_SOCKET_AF_IPV4 0 +#define MDM_HL78XX_SOCKET_AF_IPV6 1 +#if defined(CONFIG_MODEM_HL78XX_ADDRESS_FAMILY_IPV4V6) +#define MODEM_HL78XX_ADDRESS_FAMILY ADDRESS_FAMILY_IPV4V6 +#define MODEM_HL78XX_ADDRESS_FAMILY_FORMAT "####:####:####:####:####:####:####:####" +#define MODEM_HL78XX_ADDRESS_FAMILY_FORMAT_LEN \ + sizeof("a01.a02.a03.a04.a05.a06.a07.a08.a09.a10.a11.a12.a13.a14.a15.a16") +#elif defined(CONFIG_MODEM_HL78XX_ADDRESS_FAMILY_IPV4) +#define MODEM_HL78XX_ADDRESS_FAMILY ADDRESS_FAMILY_IPV4 +#define MODEM_HL78XX_ADDRESS_FAMILY_FORMAT "###.###.###.###" +#define MODEM_HL78XX_ADDRESS_FAMILY_FORMAT_LEN sizeof(MODEM_HL78XX_ADDRESS_FAMILY_FORMAT) + +#else +#define MODEM_HL78XX_ADDRESS_FAMILY ADDRESS_FAMILY_IPV6 +#endif + +/* Modem Communication Patterns */ +#define EOF_PATTERN "--EOF--Pattern--" +#define EOF_PATTERN_GNSS "+++" +#define CONNECT_STRING "CONNECT" +#define OK_STRING "OK" + +/* RAT (Radio Access Technology) commands */ +#define SET_RAT_M1_CMD_LEGACY "AT+KSRAT=0" +#define SET_RAT_NB1_CMD_LEGACY "AT+KSRAT=1" +#define SET_RAT_GSM_CMD_LEGACY "AT+KSRAT=2" +#define SET_RAT_NBNTN_CMD_LEGACY "AT+KSRAT=3" + +#define KSRAT_QUERY "AT+KSRAT?" +#define DISABLE_RAT_AUTO "AT+KSELACQ=0,0" + +#define SET_RAT_M1_CMD "AT+KSRAT=0,1" +#define SET_RAT_NB1_CMD "AT+KSRAT=1,1" +#define SET_RAT_GMS_CMD "AT+KSRAT=2,1" +#define SET_RAT_NBNTN_CMD "AT+KSRAT=3,1" + +/* Power mode commands */ +#define SET_AIRPLANE_MODE_CMD_LEGACY "AT+CFUN=4,0" +#define SET_AIRPLANE_MODE_CMD "AT+CFUN=4,1" +#define SET_FULLFUNCTIONAL_MODE_CMD_LEGACY "AT+CFUN=1,0" +#define SET_FULLFUNCTIONAL_MODE_CMD "AT+CFUN=1,1" +#define GET_FULLFUNCTIONAL_MODE_CMD "AT+CFUN?" +/* PDP Context commands */ +#define DEACTIVATE_PDP_CONTEXT "AT+CGACT=0" +#define ACTIVATE_PDP_CONTEXT "AT+CGACT=1" + +/* Helper macros */ +#define ATOI(s_, value_, desc_) modem_atoi(s_, value_, desc_, __func__) + +/* Enums */ + +enum hl78xx_gnss_event { + HL78XX_GNSS_EVENT_INVALID = -1, + HL78XX_GNSS_EVENT_INIT, + HL78XX_GNSS_EVENT_START, + HL78XX_GNSS_EVENT_STOP, + HL78XX_GNSS_EVENT_POSITION, +}; + +enum hl78xx_gnss_status { + HL78XX_GNSS_STATUS_INVALID = -1, + HL78XX_GNSS_STATUS_FAILURE, + HL78XX_GNSS_STATUS_SUCCESS, +}; + +enum hl78xx_gnss_position_event { + HL78XX_GNSS_POSITION_EVENT_INVALID = -1, + HL78XX_GNSS_POSITION_EVENT_LOST_OR_NOT_AVAILABLE_YET, + HL78XX_GNSS_POSITION_EVENT_PREDICTION_AVAILABLE, + HL78XX_GNSS_POSITION_EVENT_2D_AVAILABLE, + HL78XX_GNSS_POSITION_EVENT_3D_AVAILABLE, + HL78XX_GNSS_POSITION_EVENT_FIXED_TO_INVALID, + HL78XX_GNSS_POSITION_EVENT_SATELLITE_TIMEOUT, +}; + +enum hl78xx_state { + MODEM_HL78XX_STATE_IDLE = 0, + MODEM_HL78XX_STATE_RESET_PULSE, + MODEM_HL78XX_STATE_POWER_ON_PULSE, + MODEM_HL78XX_STATE_AWAIT_POWER_ON, + MODEM_HL78XX_STATE_SET_BAUDRATE, + MODEM_HL78XX_STATE_RUN_INIT_SCRIPT, + MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT, + MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT, + MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT, + /* Full functionality, searching + * CFUN=1 + */ + MODEM_HL78XX_STATE_AWAIT_REGISTERED, + MODEM_HL78XX_STATE_CARRIER_ON, + /* Minimum functionality, SIM powered off, Modem Power down + * CFUN=0 + */ + MODEM_HL78XX_STATE_CARRIER_OFF, + MODEM_HL78XX_STATE_SIM_POWER_OFF, + /* Minimum functionality / Airplane mode + * Sim still powered on + * CFUN=4 + */ + MODEM_HL78XX_STATE_AIRPLANE, + MODEM_HL78XX_STATE_INIT_POWER_OFF, + MODEM_HL78XX_STATE_POWER_OFF_PULSE, + MODEM_HL78XX_STATE_AWAIT_POWER_OFF, +}; + +enum hl78xx_event { + MODEM_HL78XX_EVENT_RESUME = 0, + MODEM_HL78XX_EVENT_SUSPEND, + MODEM_HL78XX_EVENT_SCRIPT_SUCCESS, + MODEM_HL78XX_EVENT_SCRIPT_FAILED, + MODEM_HL78XX_EVENT_SCRIPT_REQUIRE_RESTART, + MODEM_HL78XX_EVENT_TIMEOUT, + MODEM_HL78XX_EVENT_REGISTERED, + MODEM_HL78XX_EVENT_DEREGISTERED, + MODEM_HL78XX_EVENT_BUS_OPENED, + MODEM_HL78XX_EVENT_BUS_CLOSED, + MODEM_HL78XX_EVENT_SOCKET_READY, +}; +struct kselacq_syntax { + bool mode; + enum hl78xx_cell_rat_mode rat1; + enum hl78xx_cell_rat_mode rat2; + enum hl78xx_cell_rat_mode rat3; +}; +struct kband_syntax { + uint8_t rat; + /* Max 64 digits representation format is supported + * i.e: LTE Band 256 (2000MHz) : + * 80000000 00000000 00000000 00000000 + * 00000000 00000000 00000000 00000000 + * + + * NULL terminate + */ + uint8_t bnd_bitmap[MDM_BAND_HEX_STR_LEN]; +}; + +struct registration_status { + bool is_registered; + enum hl78xx_registration_status network_state; + enum hl78xx_cell_rat_mode rat_mode; +}; +/* driver data */ +struct modem_buffers { + uint8_t uart_rx[CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES]; + uint8_t uart_tx[CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES]; + uint8_t chat_rx[CONFIG_MODEM_HL78XX_CHAT_BUFFER_SIZES]; + uint8_t *delimiter; + uint8_t *filter; + uint8_t *argv[32]; + uint8_t *eof_pattern; + uint8_t eof_pattern_size; +}; + +struct modem_identity { + uint8_t imei[MDM_IMEI_LENGTH]; + uint8_t model_id[MDM_MODEL_LENGTH]; + uint8_t imsi[MDM_IMSI_LENGTH]; + uint8_t iccid[MDM_ICCID_LENGTH]; + uint8_t manufacturer[MDM_MANUFACTURER_LENGTH]; + uint8_t fw_version[MDM_REVISION_LENGTH]; + char apn[MDM_APN_MAX_LENGTH]; +}; + +struct modem_status { + struct registration_status registration; + uint8_t rssi; + uint8_t ksrep; + uint8_t rsrp; + uint8_t rsrq; + uint16_t script_fail_counter; + int variant; + enum hl78xx_state state; + struct kband_syntax kbndcfg[HL78XX_RAT_COUNT]; + enum hl78xx_phone_functionality phone_functionality; +}; + +struct modem_gpio_callbacks { + struct gpio_callback vgpio_cb; + struct gpio_callback uart_dsr_cb; + struct gpio_callback gpio6_cb; + struct gpio_callback uart_cts_cb; +}; + +struct modem_event_system { + struct k_work event_dispatch_work; + uint8_t event_buf[8]; + struct ring_buf event_rb; + struct k_mutex event_rb_lock; +}; + +struct hl78xx_data { + struct modem_pipe *uart_pipe; + struct modem_backend_uart uart_backend; + struct modem_chat chat; + + struct k_mutex tx_lock; + struct k_sem script_stopped_sem_tx_int; + struct k_sem script_stopped_sem_rx_int; + struct k_sem suspended_sem; + + struct modem_buffers buffers; + struct modem_identity identity; + struct modem_status status; + struct modem_gpio_callbacks gpio_cbs; + struct modem_event_system events; + + struct k_work_delayable timeout_work; + +#if defined(CONFIG_MODEM_HL78XX_RSSI_WORK) + struct k_work_delayable rssi_query_work; +#endif + + const struct device *dev; + struct kselacq_syntax kselacq_data; +}; + +struct hl78xx_config { + const struct device *uart; + struct gpio_dt_spec mdm_gpio_reset; + struct gpio_dt_spec mdm_gpio_wake; + struct gpio_dt_spec mdm_gpio_pwr_on; + struct gpio_dt_spec mdm_gpio_fast_shtdown; + struct gpio_dt_spec mdm_gpio_uart_dtr; + struct gpio_dt_spec mdm_gpio_uart_dsr; + struct gpio_dt_spec mdm_gpio_uart_cts; + struct gpio_dt_spec mdm_gpio_vgpio; + struct gpio_dt_spec mdm_gpio_gpio6; + struct gpio_dt_spec mdm_gpio_gpio8; + struct gpio_dt_spec mdm_gpio_sim_switch; + uint16_t power_pulse_duration_ms; + uint16_t reset_pulse_duration_ms; + uint16_t startup_time_ms; + uint16_t shutdown_time_ms; + + bool autostarts; + + const struct modem_chat_script *init_chat_script; +}; +/* socket read callback data */ +struct socket_read_data { + char *recv_buf; + size_t recv_buf_len; + struct sockaddr *recv_addr; + uint16_t recv_read_len; +}; + +/** + * @brief Check if the cellular modem is registered on the network. + * + * This function checks the modem's current registration status and + * returns true if the device is registered with a cellular network. + * + * @param data Pointer to the modem HL78xx driver data structure. + * + * @retval true if the modem is registered. + * @retval false otherwise. + */ +bool hl78xx_is_registered(struct hl78xx_data *data); + +/** + * @brief Generate a 32-bit hash from a string. + * + * Useful for generating identifiers (e.g., MAC address suffix) from a given string. + * + * @param str Input string to hash. + * @param len Length of the input string. + * + * @return 32-bit hash value. + */ +uint32_t hash32(const char *str, int len); + +/** + * @brief DNS resolution work callback. + * + * Should be used internally to handle DNS resolution events. + */ +void dns_work_cb(void); + +/** + * @brief Callback to update and handle network interface status. + * + * This function is typically scheduled as work to check and respond to changes + * in the modem's network interface state, such as registration, link readiness, + * or disconnection events. + * + * @param data Pointer to the modem HL78xx driver data structure. + */ +void iface_status_work_cb(struct hl78xx_data *data, + modem_chat_script_callback script_user_callback); + +/** + * @brief Convert a string to an integer with error handling. + * + * Similar to atoi, but allows specifying an error fallback and logs errors. + * + * @param s Input string to convert. + * @param err_value Value to return on failure. + * @param desc Description of the value for logging purposes. + * @param func Function name for logging purposes. + * + * @return Converted integer on success, or err_value on failure. + */ +int modem_atoi(const char *s, const int err_value, const char *desc, const char *func); + +/** + * @brief Initialize sockets for the modem. + * + * Sets up the socket table and configuration for socket communication. + * + * @param data Pointer to the modem HL78xx driver data structure. + */ +void hl78xx_socket_init(struct hl78xx_data *data); + +/** + * @brief Notify the system of socket data changes. + * + * Typically used when data has been received or transmitted on a socket. + * + * @param socket_id ID of the affected socket. + * @param new_total New data count or buffer level associated with the socket. + */ +void socknotifydata(int socket_id, int new_total); + +/** + * @brief Send a command to the modem and wait for matching response(s). + * + * This function sends a raw command to the modem and processes its response using + * the provided match patterns. It supports asynchronous notification via callback. + * + * @param data Pointer to the modem HL78xx driver data structure. + * @param script_user_callback Callback function invoked on matched responses or errors. + * @param cmd Pointer to the command buffer to send. + * @param cmd_len Length of the command in bytes. + * @param response_matches Array of expected response match patterns. + * @param matches_size Number of elements in the response_matches array. + * + * @return 0 on success, negative errno code on failure. + */ +int modem_cmd_send_int(struct hl78xx_data *data, modem_chat_script_callback script_user_callback, + const uint8_t *cmd, uint16_t cmd_len, + const struct modem_chat_match *response_matches, uint16_t matches_size, + bool user_cmd); + +/** + * @brief Find a memory block inside another block (C99-compatible version). + * + * Searches for the first occurrence of the byte string needle of length needlelen + * in the memory area haystack of length haystacklen. + * + * @param haystack Pointer to the memory block to search within. + * @param haystacklen Length of the haystack memory block. + * @param needle Pointer to the memory block to search for. + * @param needlelen Length of the needle memory block. + * + * @return Pointer to the beginning of the found needle, or NULL if not found. + */ +const void *c99_memmem(const void *haystack, size_t haystacklen, const void *needle, + size_t needlelen); + +/** + * @brief Generate a pseudo-random MAC address based on the modem's IMEI. + * + * This function creates a MAC address using a fixed prefix and a hash of the IMEI. + * The resulting address is consistent for the same IMEI and suitable for use + * in virtual or emulated network interfaces. + * + * @param mac_addr Pointer to a 6-byte buffer where the generated MAC address will be stored. + * @param imei Null-terminated string containing the modem's IMEI. + * + * @return Pointer to the MAC address buffer. + */ +static inline uint8_t *modem_get_mac(uint8_t *mac_addr, char *imei) +{ + uint32_t hash_value; + /* Define MAC address prefix */ + mac_addr[0] = 0x00; + mac_addr[1] = 0x10; + + /* Generate MAC address based on IMEI */ + hash_value = hash32(imei, strlen(imei)); + UNALIGNED_PUT(hash_value, (uint32_t *)(mac_addr + 2)); + + return mac_addr; +} + +/** + * @brief Handle modem state update from +KSTATE URC (unsolicited result code). + * + * This function is called when a +KSTATE URC is received, indicating a change + * in the modem's internal state. It updates the modem driver's state machine + * accordingly. + * + * @param data Pointer to the HL78xx modem driver data structure. + * @param state Integer value representing the new modem state as reported by the URC. + */ +void hl78xx_on_kstatev_parser(struct hl78xx_data *data, int state); + +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_ICCID) || defined(CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI) +/** + * @brief Automatically detect and configure the modem's APN setting. + * + * Uses internal logic to determine the correct APN based on the modem's context + * and network registration information. + * + * @param data Pointer to the modem HL78xx driver data structure. + * @param associated_number Identifier (e.g., MCCMNC or IMSI) used for APN detection. + * + * @return 0 on success, negative errno code on failure. + */ +int modem_detect_apn(struct hl78xx_data *data, const char *associated_number); +#endif +/** + * @brief Get the default band configuration in hexadecimal string format for each band. + * + * Retrieves the modem's default band configuration as a hex string, + * used for configuring or restoring band settings. + * + * @param rat The radio access technology mode for which to get the band configuration. + * @param hex_bndcfg Buffer to store the resulting hex band configuration string. + * @param size_in_bytes Size of the buffer in bytes. + * + * @retval 0 on success. + * @retval Negative errno code on failure. + */ +int hl78xx_get_band_default_config_for_rat(enum hl78xx_cell_rat_mode rat, char *hex_bndcfg, + size_t size_in_bytes); +/** + * @brief Trim leading zeros from a hexadecimal string. + * + * Removes any '0' characters from the beginning of the provided hex string, + * returning a pointer to the first non-zero character. + * + * @param hex_str Null-terminated hexadecimal string. + * + * @return Pointer to the first non-zero digit in the string, + * or the last zero if the string is all zeros. + */ +const char *hl78xx_trim_leading_zeros(const char *hex_str); +/** + * @brief Convert a binary bitmap to a trimmed hexadecimal string. + * + * Converts a bitmap into a hex string, removing leading zeros for a + * compact representation. Useful for modem configuration commands. + * + * @param bitmap Pointer to the input binary bitmap. + * @param hex_str Output buffer for the resulting hex string. + * @param hex_str_len Size of the output buffer in bytes. + */ +void hl78xx_bitmap_to_hex_string_trimmed(const uint8_t *bitmap, char *hex_str, size_t hex_str_len); +/** + * @brief Convert a hexadecimal string to a binary bitmap. + * + * Parses a hexadecimal string and converts it into a binary bitmap array. + * + * @param hex_str Null-terminated string containing hexadecimal data. + * @param bitmap_out Output buffer to hold the resulting binary bitmap. + * + * @retval 0 on success. + * @retval Negative errno code on failure (e.g., invalid characters, overflow). + */ +int hl78xx_hex_string_to_bitmap(const char *hex_str, uint8_t *bitmap_out); + +void hl78xx_extract_essential_part_apn(const char *full_apn, char *essential_apn, size_t max_len); + +int hl78xx_set_apn_internal(struct hl78xx_data *data, const char *apn, uint16_t size); + +int hl78xx_api_func_set_phone_functionality(const struct device *dev, + enum hl78xx_phone_functionality functionality, + bool reset); + +int hl78xx_api_func_get_phone_functionality(const struct device *dev, + enum hl78xx_phone_functionality *functionality); + +int hl78xx_api_func_get_signal(const struct device *dev, const enum hl78xx_signal_type type, + int16_t *value); + +int hl78xx_api_func_get_registration_status(const struct device *dev, + enum hl78xx_cell_rat_mode *tech, + enum hl78xx_registration_status *status); + +int hl78xx_api_func_get_modem_info(const struct device *dev, enum hl78xx_modem_info_type type, + char *info, size_t size); + +int hl78xx_api_func_set_apn(const struct device *dev, const char *apn, uint16_t size); + +int hl78xx_api_func_modem_cmd_send_int(const struct device *dev, const char *cmd, uint16_t cmd_size, + const struct modem_chat_match *response_matches, + uint16_t matches_size); + +void hl78xx_enter_state(struct hl78xx_data *data, enum hl78xx_state state); + +void hl78xx_delegate_event(struct hl78xx_data *data, enum hl78xx_event evt); + +void notif_carrier_off(void); +int check_if_any_socket_connected(void); + +#endif /* HL78XX_H */ diff --git a/drivers/modem/hl78xx/hl78xx_apis.c b/drivers/modem/hl78xx/hl78xx_apis.c new file mode 100644 index 000000000000..07c60188e37a --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_apis.c @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include +#include +#include +#include +#include +#include "hl78xx.h" + +LOG_MODULE_REGISTER(hl78xx_apis, CONFIG_MODEM_LOG_LEVEL); + +static void hl78xx_chat_callback_handler(struct modem_chat *chat, + enum modem_chat_script_result result, void *user_data) +{ + if (result == MODEM_CHAT_SCRIPT_RESULT_SUCCESS) { +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d RUN MODEM_CHAT_SCRIPT_RESULT_SUCCESS", __LINE__); +#endif + } else { +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d RUN MODEM_CHAT_SCRIPT_RESULT_FAIL", __LINE__); +#endif + } +} + +static void hl78xx_on_cmerror(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %s", __LINE__, __func__, argv[0]); +#endif +} + +static void hl78xx_on_ok(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %s", __LINE__, __func__, argv[0]); +#endif +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", hl78xx_on_ok); +MODEM_CHAT_MATCHES_DEFINE(allow_match, MODEM_CHAT_MATCH("OK", "", hl78xx_on_ok), + MODEM_CHAT_MATCH("+CME ERROR: ", "", hl78xx_on_cmerror)); + +int hl78xx_api_func_get_signal(const struct device *dev, const enum hl78xx_signal_type type, + int16_t *value) +{ + int ret = -ENOTSUP; + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + const char *signal_cmd_csq = "AT+CSQ"; + const char *signal_cmd_cesq = "AT+CESQ"; + + if (data->status.state != MODEM_HL78XX_STATE_CARRIER_ON) { + return -ENODATA; + } + + /* Run chat script */ + switch (type) { + case HL78XX_SIGNAL_RSSI: + ret = modem_cmd_send_int(data, hl78xx_chat_callback_handler, signal_cmd_csq, + strlen(signal_cmd_csq), allow_match, 2, true); + break; + + case HL78XX_SIGNAL_RSRP: + case HL78XX_SIGNAL_RSRQ: + ret = modem_cmd_send_int(data, hl78xx_chat_callback_handler, signal_cmd_cesq, + strlen(signal_cmd_cesq), allow_match, 2, true); + break; + + default: + ret = -ENOTSUP; + break; + } + + /* Verify chat script ran successfully */ + if (ret < 0) { + return ret; + } + + /* Parse received value */ + switch (type) { + case HL78XX_SIGNAL_RSSI: + ret = hl78xx_parse_rssi(data->status.rssi, value); + break; + + case HL78XX_SIGNAL_RSRP: + ret = hl78xx_parse_rsrp(data->status.rsrp, value); + break; + + case HL78XX_SIGNAL_RSRQ: + ret = hl78xx_parse_rsrq(data->status.rsrq, value); + break; + + default: + ret = -ENOTSUP; + break; + } + + return ret; +} + +int hl78xx_api_func_get_registration_status(const struct device *dev, + enum hl78xx_cell_rat_mode *tech, + enum hl78xx_registration_status *status) +{ + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + if (tech == NULL || status == NULL) { + return -EINVAL; + } + + *tech = data->status.registration.rat_mode; + *status = data->status.registration.network_state; + + return 0; +} + +int hl78xx_api_func_get_modem_info(const struct device *dev, enum hl78xx_modem_info_type type, + char *info, size_t size) +{ + int ret = 0; + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + if (info == NULL || size == 0) { + return -EINVAL; + } + + switch (type) { + case HL78XX_MODEM_INFO_IMEI: + strncpy(info, data->identity.imei, MIN(size, sizeof(data->identity.imei))); + break; + case HL78XX_MODEM_INFO_SIM_IMSI: + strncpy(info, data->identity.imsi, MIN(size, sizeof(data->identity.imsi))); + break; + case HL78XX_MODEM_INFO_MANUFACTURER: + strncpy(info, data->identity.manufacturer, + MIN(size, sizeof(data->identity.manufacturer))); + break; + case HL78XX_MODEM_INFO_FW_VERSION: + strncpy(info, data->identity.fw_version, + MIN(size, sizeof(data->identity.fw_version))); + break; + case HL78XX_MODEM_INFO_MODEL_ID: + strncpy(info, data->identity.model_id, MIN(size, sizeof(data->identity.model_id))); + break; + case HL78XX_MODEM_INFO_SIM_ICCID: + strncpy(info, data->identity.iccid, MIN(size, sizeof(data->identity.iccid))); + break; + case HL78XX_MODEM_INFO_APN: + strncpy(info, data->identity.apn, MIN(size, sizeof(data->identity.apn))); + break; + default: + ret = -ENOTSUP; + break; + } + + return ret; +} + +int hl78xx_set_apn_internal(struct hl78xx_data *data, const char *apn, uint16_t size) +{ + int ret = 0; + char cmd_string[sizeof("AT+KCNXCFG=,\"\",\"\"") + sizeof(uint8_t) + + sizeof(MODEM_HL78XX_ADDRESS_FAMILY) + MDM_APN_MAX_LENGTH] = {0}; + int cmd_max_len = sizeof(cmd_string) - 1; + int apn_size = strlen(apn); + + if (apn == NULL || size >= MDM_APN_MAX_LENGTH) { + return -EINVAL; + } + + if (strncmp(data->identity.apn, apn, apn_size) != 0) { + strncpy(data->identity.apn, apn, apn_size); + } + /* check if the pdp is active, if yes, disable it first.*/ + /* Important: Deactivating all PDP contexts (e.g. by using AT+CGACT=0 with no + * parameters) also causes the device to detach from the network (equivalent to + * AT+CGATT=0) + * Theorically it is also equivalent to at+cfun=4 + * to keep sync use SET_AIRPLANE_MODE_CMD_LEGACY to deactivate pdp context if you have + * only one pdp context + */ + snprintk(cmd_string, cmd_max_len, "AT+CGDCONT=1,\"%s\",\"%s\"", MODEM_HL78XX_ADDRESS_FAMILY, + apn); + + ret = modem_cmd_send_int(data, NULL, cmd_string, strlen(cmd_string), &ok_match, 1, true); + if (ret < 0) { + goto error; + } + + snprintk(cmd_string, cmd_max_len, + "AT+KCNXCFG=1,\"GPRS\",\"%s\",,,\"" MODEM_HL78XX_ADDRESS_FAMILY "\"", apn); + + ret = modem_cmd_send_int(data, NULL, cmd_string, strlen(cmd_string), &ok_match, 1, true); + if (ret < 0) { + goto error; + } + +error: + return ret; +} + +int hl78xx_api_func_set_apn(const struct device *dev, const char *apn, uint16_t size) +{ + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + if (apn == NULL || size > MDM_APN_MAX_LENGTH) { + return -EINVAL; + } + + strncpy(data->identity.apn, apn, sizeof(data->identity.apn)); + hl78xx_enter_state(data, MODEM_HL78XX_STATE_CARRIER_OFF); + return 0; +} + +int hl78xx_api_func_set_phone_functionality(const struct device *dev, + enum hl78xx_phone_functionality functionality, + bool reset) +{ + char cmd_string[sizeof(SET_FULLFUNCTIONAL_MODE_CMD) + sizeof(int)] = {0}; + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + /* configure modem fully fuctinal without restart */ + + snprintf(cmd_string, sizeof(cmd_string), "AT+CFUN=%d,%d", functionality, reset); + + return modem_cmd_send_int(data, NULL, cmd_string, strlen(cmd_string), &ok_match, 1, true); +} + +int hl78xx_api_func_get_phone_functionality(const struct device *dev, + enum hl78xx_phone_functionality *functionality) +{ + const char *cmd_string = GET_FULLFUNCTIONAL_MODE_CMD; + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + /* configure modem fully fuctinal without restart */ + return modem_cmd_send_int(data, NULL, cmd_string, strlen(cmd_string), &ok_match, 1, true); +} + +int hl78xx_api_func_modem_cmd_send_int(const struct device *dev, const char *cmd, uint16_t cmd_size, + const struct modem_chat_match *response_matches, + uint16_t matches_size) +{ + + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + return modem_cmd_send_int(data, NULL, cmd, cmd_size, response_matches, 1, true); +} diff --git a/drivers/modem/hl78xx/hl78xx_evt_monitor/CMakeLists.txt b/drivers/modem/hl78xx/hl78xx_evt_monitor/CMakeLists.txt new file mode 100644 index 000000000000..ea00a6b9cef7 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/CMakeLists.txt @@ -0,0 +1,10 @@ +# +# Copyright (c) 2025 Netfeasa Ltd. +# +# SPDX-License-Identifier: Apache-2.0 +# + +zephyr_library() +zephyr_library_sources(hl78xx_evt_monitor.c) +# Event monitors data must be in RAM +zephyr_linker_sources(RWDATA hl78xx_evt_monitor.ld) diff --git a/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor b/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor new file mode 100644 index 000000000000..e002f24ec2c1 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor @@ -0,0 +1,29 @@ +# +# Copyright (c) 2025 Netfeasa Ltd. +# +# SPDX-License-Identifier: Apache-2.0 +# + +menuconfig HL78XX_EVT_MONITOR + bool "HL78XX AT notification monitor" + +if HL78XX_EVT_MONITOR + +config HL78XX_EVT_MONITOR_HEAP_SIZE + int "Heap size for notifications" + range 64 4096 + default 256 + +config HL78XX_EVT_MONITOR_APP_INIT_PRIORITY + int "Sierra Wireless HL78XX event monitor app init priority" + default 0 + help + Sierra Wireless HL78XX event monitor app initialization priority. + Do not mess with it unless you know what you are doing. + +module=HL78XX_EVT_MONITOR +module-dep=LOG +module-str= Event notification monitor library +source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config" + +endif # HL78XX_EVT_MONITOR diff --git a/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.c b/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.c new file mode 100644 index 000000000000..14f1d2bd8100 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.c @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(hl78xx_evt_monitor, CONFIG_HL78XX_EVT_MONITOR_LOG_LEVEL); + +struct evt_notif_fifo { + void *fifo_reserved; + struct hl78xx_evt data; +}; + +static void hl78xx_evt_monitor_task(struct k_work *work); + +static K_FIFO_DEFINE(hl78xx_evt_monitor_fifo); +static K_HEAP_DEFINE(hl78xx_evt_monitor_heap, CONFIG_HL78XX_EVT_MONITOR_HEAP_SIZE); +static K_WORK_DEFINE(hl78xx_evt_monitor_work, hl78xx_evt_monitor_task); + +static bool is_paused(const struct hl78xx_evt_monitor_entry *mon) +{ + return mon->flags.paused; +} + +static bool is_direct(const struct hl78xx_evt_monitor_entry *mon) +{ + return mon->flags.direct; +} + +/* Dispatch EVT notifications immediately, or schedules a workqueue task to do that. + * Keep this function public so that it can be called by tests. + * This function is called from an ISR. + */ +void hl78xx_evt_monitor_dispatch(struct hl78xx_evt *notif) +{ + bool monitored; + struct evt_notif_fifo *evt_notif; + size_t sz_needed; + + __ASSERT_NO_MSG(notif != NULL); + + monitored = false; + STRUCT_SECTION_FOREACH(hl78xx_evt_monitor_entry, e) { + if (!is_paused(e)) { + if (is_direct(e)) { + LOG_DBG("Dispatching to %p (ISR)", e->handler); + e->handler(notif); + } else { + /* Copy and schedule work-queue task */ + monitored = true; + } + } + } + + if (!monitored) { + /* Only copy monitored notifications to save heap */ + return; + } + + sz_needed = sizeof(struct evt_notif_fifo) + sizeof(notif); + + evt_notif = k_heap_alloc(&hl78xx_evt_monitor_heap, sz_needed, K_NO_WAIT); + if (!evt_notif) { + LOG_WRN("No heap space for incoming notification: %d", notif->type); + __ASSERT(evt_notif, "No heap space for incoming notification: %d", notif->type); + return; + } + + evt_notif->data = *notif; + + k_fifo_put(&hl78xx_evt_monitor_fifo, evt_notif); + k_work_submit(&hl78xx_evt_monitor_work); +} + +static void hl78xx_evt_monitor_task(struct k_work *work) +{ + struct evt_notif_fifo *evt_notif; + + while ((evt_notif = k_fifo_get(&hl78xx_evt_monitor_fifo, K_NO_WAIT))) { + /* Dispatch notification with all monitors */ + LOG_DBG("EVT notif: %d", evt_notif->data.type); + STRUCT_SECTION_FOREACH(hl78xx_evt_monitor_entry, e) { + if (!is_paused(e) && !is_direct(e)) { + LOG_DBG("Dispatching to %p", e->handler); + e->handler(&evt_notif->data); + } + } + k_heap_free(&hl78xx_evt_monitor_heap, evt_notif); + } +} + +static int hl78xx_evt_monitor_sys_init(void) +{ + int err = 0; + + err = hl78xx_evt_notif_handler_set(hl78xx_evt_monitor_dispatch); + if (err) { + LOG_ERR("Failed to hook the dispatch function, err %d", err); + } + + return 0; +} + +/* Initialize during SYS_INIT */ +SYS_INIT(hl78xx_evt_monitor_sys_init, APPLICATION, CONFIG_HL78XX_EVT_MONITOR_APP_INIT_PRIORITY); diff --git a/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.ld b/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.ld new file mode 100644 index 000000000000..e92793a5adc7 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.ld @@ -0,0 +1,5 @@ +/*HL78XX event monitors */ +. = ALIGN(4); +_hl78xx_evt_monitor_entry_list_start = .; +KEEP(*(SORT_BY_NAME("._hl78xx_evt_monitor_entry.*"))); +_hl78xx_evt_monitor_entry_list_end = .; diff --git a/drivers/modem/hl78xx/hl78xx_sockets.c b/drivers/modem/hl78xx/hl78xx_sockets.c new file mode 100644 index 000000000000..7c72738a3258 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_sockets.c @@ -0,0 +1,1624 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "hl78xx.h" + +LOG_MODULE_REGISTER(hl78xx_socket, CONFIG_MODEM_LOG_LEVEL); + +/* Helper macros and constants */ +#define CGCONTRDP_RESPONSE_NUM_DELIMS 7 +#define MDM_IP_INFO_RESP_SIZE 256 + +#define MODEM_STREAM_STARTER_WORD "\r\n" CONNECT_STRING "\r\n" +#define MODEM_STREAM_END_WORD "\r\n" OK_STRING "\r\n" + +#define MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT (0) + +#define DNS_SERVERS_COUNT \ + (0 + (IS_ENABLED(CONFIG_NET_IPV6) ? 1 : 0) + (IS_ENABLED(CONFIG_NET_IPV4) ? 1 : 0) + \ + 1 /* for NULL terminator */ \ + ) +RING_BUF_DECLARE(mdm_recv_pool, CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES); + +/* ---------------- Global Data Structures ---------------- */ +struct hl78xx_socket_data { + struct net_if *net_iface; + uint8_t mac_addr[6]; + + /* socket data */ + struct modem_socket_config socket_config; + struct modem_socket sockets[MDM_MAX_SOCKETS]; + int current_sock_fd; + int sizeof_socket_data; + int requested_socket_id; + + char dns_v4_string[NET_IPV4_ADDR_LEN]; + char dns_v6_string[NET_IPV6_ADDR_LEN]; + bool dns_ready; + struct in_addr ipv4Addr; + struct in_addr subnet; + struct in_addr gateway; + struct in_addr dns_v4; + struct in_addr new_ipv4_addr; + struct in6_addr dns_v6; + /* rx net buffer */ + struct ring_buf *buf_pool; + uint32_t expected_buf_len; + uint32_t collected_buf_len; + + struct hl78xx_data *mdata_global; +}; +struct work_socket_data { + char buf[32]; + uint16_t len; +}; + +struct receive_socket_data { + char buf[MDM_MAX_DATA_LENGTH + ARRAY_SIZE(MODEM_STREAM_STARTER_WORD) + + ARRAY_SIZE(MODEM_STREAM_END_WORD)]; + uint16_t len; +}; + +uint8_t *buf_argv[32]; +struct work_socket_data work_buf; +struct receive_socket_data receive_buf; + +uint16_t match_len; + +bool match_connect_found; +bool match_eof_ok_found; +bool match_found; +bool socket_data_received; + +uint16_t start_index_eof; +uint16_t size_of_socketdata; +atomic_t state_leftover; +struct hl78xx_socket_data socket_data; + +static int offload_socket(int family, int type, int protom); + +static void hl78xx_on_kudpsnd(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct modem_socket *sock = NULL; + int id; + + /* look up new socket by special id */ + sock = modem_socket_from_newid(&socket_data.socket_config); + if (sock) { + id = ATOI(argv[1], -1, "socket_id"); + sock->is_connected = true; + + /* on error give up modem socket */ + if (modem_socket_id_assign(&socket_data.socket_config, sock, id) < 0) { + + modem_socket_put(&socket_data.socket_config, sock->sock_fd); + } + } +} + +/* Handler: +USOCR: [0] */ +static void hl78xx_on_cmd_sockcreate(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct modem_socket *sock = NULL; + int socket_id; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + /* look up new socket by special id */ + sock = modem_socket_from_newid(&socket_data.socket_config); + if (sock) { + socket_id = ATOI(argv[1], -1, "socket_id"); + /* on error give up modem socket */ + if (modem_socket_id_assign(&socket_data.socket_config, sock, socket_id) < 0) { + modem_socket_put(&socket_data.socket_config, sock->sock_fd); + } + } + /* don't give back semaphore -- OK to follow */ +} + +void hl78xx_on_kstatev_parser(struct hl78xx_data *data, int state) +{ +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("KSTATEV: socket %d state %d", socket_data.current_sock_fd, state); +#endif + switch (state) { + case 1: /* CLOSED */ + LOG_DBG("Socket fd: %d closed by modem (KSTATEV: 2 1), resetting socket", + socket_data.current_sock_fd); + /* Mark socket as closed to block future I/O */ + /* Free socket resources */ + /* Set a global flag to indicate reconnect is needed */ + break; + case 3: + LOG_DBG("Socket %d connected", socket_data.current_sock_fd); + break; + case 5: + LOG_DBG("Data ready on socket %d", socket_data.current_sock_fd); + break; + default: + LOG_DBG("Unhandled KSTATEV for socket %d state %d", socket_data.current_sock_fd, + state); + break; + } +} + +static bool parse_ip(bool is_ipv4, const char *ip_str, void *out_addr) +{ + int ret = net_addr_pton(is_ipv4 ? AF_INET : AF_INET6, ip_str, out_addr); + + LOG_DBG("Parsing %s address: %s -> %s", is_ipv4 ? "IPv4" : "IPv6", ip_str, + (ret < 0) ? "FAIL" : "OK"); + if (ret < 0) { + LOG_ERR("Invalid IP address: %s", ip_str); + return false; + } + return true; +} + +static bool update_dns(bool is_ipv4, const char *dns_str) +{ + int ret; + + LOG_DBG("Updating DNS (%s): %s", is_ipv4 ? "IPv4" : "IPv6", dns_str); + + if (is_ipv4) { + ret = strncmp(dns_str, socket_data.dns_v4_string, strlen(dns_str)); + if (ret != 0) { + LOG_DBG("New IPv4 DNS differs from current, marking dns_ready = false"); + socket_data.dns_ready = false; + } + strncpy(socket_data.dns_v4_string, dns_str, sizeof(socket_data.dns_v4_string)); + socket_data.dns_v4_string[sizeof(socket_data.dns_v4_string) - 1] = '\0'; + return parse_ip(true, socket_data.dns_v4_string, &socket_data.dns_v4); + } +#ifdef CONFIG_NET_IPV6 + else { + ret = strncmp(dns_str, socket_data.dns_v6_string, strlen(dns_str)); + if (ret != 0) { + LOG_DBG("New IPv6 DNS differs from current, marking dns_ready = false"); + socket_data.dns_ready = false; + } + strncpy(socket_data.dns_v6_string, dns_str, sizeof(socket_data.dns_v6_string)); + socket_data.dns_v6_string[sizeof(socket_data.dns_v6_string) - 1] = '\0'; + + if (!parse_ip(false, socket_data.dns_v6_string, &socket_data.dns_v6)) { + return false; + } + + net_addr_ntop(AF_INET6, &socket_data.dns_v6, socket_data.dns_v6_string, + sizeof(socket_data.dns_v6_string)); + LOG_DBG("Parsed IPv6 DNS: %s", socket_data.dns_v6_string); + } +#endif + return true; +} + +bool is_valid_ipv4_addr(struct in_addr *addr) +{ + return addr && (addr->s_addr != 0); +} + +static void set_iface(bool is_ipv4, struct in6_addr *new_ipv6_addr, struct in6_addr *ipv6Addr) +{ + if (!socket_data.net_iface) { + LOG_DBG("No network interface set. Skipping iface config."); + return; + } + + LOG_DBG("Setting %s interface address...", is_ipv4 ? "IPv4" : "IPv6"); + + if (is_ipv4) { +#ifdef CONFIG_NET_IPV4 + if (is_valid_ipv4_addr(&socket_data.ipv4Addr)) { + net_if_ipv4_addr_rm(socket_data.net_iface, &socket_data.ipv4Addr); + } + + if (!net_if_ipv4_addr_add(socket_data.net_iface, &socket_data.new_ipv4_addr, + NET_ADDR_DHCP, 0)) { + LOG_ERR("Failed to set IPv4 interface address."); + } + + net_if_ipv4_set_netmask_by_addr(socket_data.net_iface, &socket_data.new_ipv4_addr, + &socket_data.subnet); + net_if_ipv4_set_gw(socket_data.net_iface, &socket_data.gateway); + + net_ipaddr_copy(&socket_data.ipv4Addr, &socket_data.new_ipv4_addr); + LOG_DBG("IPv4 interface configuration complete."); +#endif + } else { +#ifdef CONFIG_NET_IPV6 + net_if_ipv6_addr_rm(socket_data.net_iface, ipv6Addr); + + if (!net_if_ipv6_addr_add(socket_data.net_iface, new_ipv6_addr, NET_ADDR_AUTOCONF, + 0)) { + LOG_ERR("Failed to set IPv6 interface address."); + } else { + LOG_DBG("IPv6 interface configuration complete."); + } +#endif + } +} + +static bool split_ipv4_and_subnet(const char *combined, char *ip_out, size_t ip_out_len, + char *subnet_out, size_t subnet_out_len) +{ + int dot_count = 0; + const char *ptr = combined; + const char *split = NULL; + + while (*ptr && dot_count < 4) { + if (*ptr == '.') { + dot_count++; + if (dot_count == 4) { + split = ptr; + break; + } + } + ptr++; + } + + if (!split) { + LOG_ERR("Invalid IPv4 + subnet format: %s", combined); + return false; + } + + size_t ip_len = split - combined; + + if (ip_len >= ip_out_len) { + ip_len = ip_out_len - 1; + } + + strncpy(ip_out, combined, ip_len); + ip_out[ip_len] = '\0'; + + strncpy(subnet_out, split + 1, subnet_out_len); + subnet_out[subnet_out_len - 1] = '\0'; + + LOG_DBG("Extracted IP: %s, Subnet: %s", ip_out, subnet_out); + return true; +} + +static void hl78xx_on_cgdcontrdp(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + if (argc < 8 || !argv[4] || !argv[5] || !argv[6]) { + LOG_ERR("Incomplete CGCONTRDP response: argc = %d", argc); + return; + } + + LOG_INF("CGCONTRDP: apn=%s addr=%s gw=%s dns=%s", argv[3], argv[4], argv[5], argv[6]); + + bool is_ipv4 = (strchr(argv[4], ':') == NULL); + + char ip_addr[NET_IPV6_ADDR_LEN]; + char subnet_mask[NET_IPV6_ADDR_LEN]; + +#ifdef CONFIG_MODEM_HL78XX_APN_SOURCE_NETWORK + bool is_apn_exists = (argv[3] && strlen(argv[3]) > 0); + + if (is_apn_exists) { + hl78xx_extract_essential_part_apn(argv[3], socket_data.mdata_global->identity.apn, + sizeof(socket_data.mdata_global->identity.apn)); + } +#endif + if (!split_ipv4_and_subnet(argv[4], ip_addr, sizeof(ip_addr), subnet_mask, + sizeof(subnet_mask))) { + return; + } + + LOG_INF("Extracted IP: %s, Subnet: %s", ip_addr, subnet_mask); + +#ifdef CONFIG_NET_IPV6 + struct in6_addr new_ipv6_addr = {0}; + struct in6_addr ipv6Addr = {0}; +#endif + + if (is_ipv4) { + if (!parse_ip(true, ip_addr, &socket_data.new_ipv4_addr)) { + return; + } + if (!parse_ip(true, subnet_mask, &socket_data.subnet)) { + return; + } + if (!parse_ip(true, argv[5], &socket_data.gateway)) { + return; + } + } else { +#ifdef CONFIG_NET_IPV6 + if (!parse_ip(false, ip_addr, &new_ipv6_addr)) { + return; + } +#endif + } + + if (!update_dns(is_ipv4, argv[6])) { + return; + } + +#ifdef CONFIG_NET_IPV6 + set_iface(is_ipv4, &new_ipv6_addr, &ipv6Addr); +#else + set_iface(is_ipv4, NULL, NULL); +#endif +} + +static void hl78xx_on_ktcpstate(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + if (argc < 4) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %s %s %s %s", __LINE__, argv[1], argv[2], argv[3], argv[4], argv[5]); +#endif + uint8_t tcp_session_id = ATOI(argv[1], 0, "tcp_session_id"); + uint8_t tcp_status = ATOI(argv[2], 0, "tcp_status"); + int8_t tcp_notif = ATOI(argv[3], 0, "tcp_notif"); + uint16_t rcv_data = ATOI(argv[5], 0, "tcp_rcv_data"); + + if (tcp_status != 3 && tcp_notif != -1) { + return; + } + + socknotifydata(tcp_session_id, rcv_data); +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); +MODEM_CHAT_MATCHES_DEFINE(connect_matches, MODEM_CHAT_MATCH(CONNECT_STRING, "", NULL), + MODEM_CHAT_MATCH("ERROR", "", NULL)); +MODEM_CHAT_MATCH_DEFINE(kudpind_match, "+KUDP_IND: ", ",", hl78xx_on_kudpsnd); +MODEM_CHAT_MATCH_DEFINE(ktcpind_match, "+KTCP_IND: ", ",", hl78xx_on_kudpsnd); +MODEM_CHAT_MATCH_DEFINE(ktcp_match, "+KTCPCFG: ", "", hl78xx_on_cmd_sockcreate); +MODEM_CHAT_MATCH_DEFINE(cgdcontrdp_match, "+CGCONTRDP: ", ",", hl78xx_on_cgdcontrdp); +MODEM_CHAT_MATCH_DEFINE(ktcp_state_match, "+KTCPSTAT: ", ",", hl78xx_on_ktcpstate); + +static void parser_reset(void) +{ + memset(&receive_buf, 0, sizeof(receive_buf)); + match_found = false; +} + +static void found_reset(void) +{ + match_connect_found = false; + match_eof_ok_found = false; + socket_data_received = false; +} + +static bool modem_chat_parse_end_del_start(struct modem_chat *chat) +{ + if (receive_buf.len == 0) { + return false; + } + + for (uint8_t i = 0; i < chat->delimiter_size; i++) { + if (receive_buf.buf[receive_buf.len - 1] == chat->delimiter[i]) { + return true; + } + } + return false; +} + +static bool modem_chat_parse_end_del_complete(struct modem_chat *chat) +{ + if (receive_buf.len < chat->delimiter_size) { + return false; + } + + return memcmp(&receive_buf.buf[receive_buf.len - chat->delimiter_size], chat->delimiter, + chat->delimiter_size) == 0; +} + +static bool modem_chat_match_matches_received(const char *match, uint16_t match_size) +{ + if (receive_buf.len < match_size) { + return false; + } + + for (uint16_t i = 0; i < match_size; i++) { + if (match[i] != receive_buf.buf[i]) { + return false; + } + } + return true; +} + +static bool is_receive_buffer_full(void) +{ + return receive_buf.len >= ARRAY_SIZE(receive_buf.buf); +} + +static void handle_expected_length_decrement(void) +{ + if (match_connect_found && socket_data.expected_buf_len > 0) { + socket_data.expected_buf_len--; + } +} + +static bool is_end_delimiter_only(void) +{ + return receive_buf.len == socket_data.mdata_global->chat.delimiter_size; +} + +static bool is_valid_eof_index(uint8_t size_match) +{ + start_index_eof = receive_buf.len - size_match - 2; + return start_index_eof < ARRAY_SIZE(receive_buf.buf); +} + +static void try_handle_eof_pattern(void) +{ + uint8_t size_match = strlen(EOF_PATTERN); + + if (receive_buf.len < size_match + 2) { + return; + } + if (!is_valid_eof_index(size_match)) { + return; + } + + if (strncmp(&receive_buf.buf[start_index_eof], EOF_PATTERN, size_match) == 0) { + int ret = ring_buf_put(socket_data.buf_pool, receive_buf.buf, start_index_eof); + + if (ret <= 0) { + LOG_ERR("ring_buf_put failed: %d", ret); + } + socket_data_received = true; + } +} + +static bool is_connect_match(void) +{ + uint8_t size_match = strlen(CONNECT_STRING); + + return receive_buf.len == size_match && + modem_chat_match_matches_received(CONNECT_STRING, size_match); +} + +static bool is_ok_match(void) +{ + uint8_t size_match = strlen(OK_STRING); + + return receive_buf.len == size_match && + modem_chat_match_matches_received(OK_STRING, size_match); +} + +static void socket_process_bytes(char byte) +{ + if (is_receive_buffer_full()) { + LOG_WRN("Receive buffer overrun"); + parser_reset(); + return; + } + + receive_buf.buf[receive_buf.len++] = byte; + handle_expected_length_decrement(); + + if (modem_chat_parse_end_del_complete(&socket_data.mdata_global->chat)) { + if (is_end_delimiter_only()) { + parser_reset(); + return; + } + + size_of_socketdata = receive_buf.len; + + if (match_connect_found && !match_eof_ok_found) { + try_handle_eof_pattern(); + } + + parser_reset(); + return; + } + + if (modem_chat_parse_end_del_start(&socket_data.mdata_global->chat)) { + return; + } + + if (!match_found && !match_connect_found && is_connect_match()) { + match_connect_found = true; + LOG_DBG("CONNECT matched. Expecting %d more bytes.", socket_data.expected_buf_len); + return; + } + + if (match_connect_found && !match_eof_ok_found && is_ok_match()) { + match_eof_ok_found = true; + LOG_DBG("OK matched."); + } +} + +static int modem_process_handler(void) +{ + if (socket_data.expected_buf_len == 0) { + LOG_DBG("No more data expected"); + atomic_set_bit(&state_leftover, MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT); + return 0; + } + + int recv_len = modem_pipe_receive(socket_data.mdata_global->uart_pipe, work_buf.buf, + MIN(sizeof(work_buf.buf), socket_data.expected_buf_len)); + if (recv_len <= 0) { + LOG_WRN("modem_pipe_receive returned %d", recv_len); + return recv_len; + } + + work_buf.len = recv_len; + LOG_HEXDUMP_DBG(work_buf.buf, recv_len, "Received bytes:"); + + for (int i = 0; i < recv_len; i++) { + socket_process_bytes(work_buf.buf[i]); + } + + if (match_eof_ok_found && socket_data_received) { + LOG_DBG("All data received: %d bytes", size_of_socketdata); + socket_data.expected_buf_len = 0; + found_reset(); + + k_sem_give(&socket_data.mdata_global->script_stopped_sem_rx_int); + } + + return 0; +} + +static void modem_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event, + void *user_data) +{ +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s Pipe event received: %d", __LINE__, __func__, event); +#endif + switch (event) { + case MODEM_PIPE_EVENT_RECEIVE_READY: + modem_process_handler(); + break; + + case MODEM_PIPE_EVENT_TRANSMIT_IDLE: + k_sem_give(&socket_data.mdata_global->script_stopped_sem_tx_int); + break; + + default: + LOG_DBG("Unhandled event: %d", event); + break; + } +} + +void notif_carrier_off(void) +{ + net_if_carrier_off(socket_data.net_iface); +} + +void iface_status_work_cb(struct hl78xx_data *data, modem_chat_script_callback script_user_callback) +{ + + char *cmd = "AT+CGCONTRDP=1"; + int ret = 0; + + ret = modem_cmd_send_int(data, script_user_callback, cmd, strlen(cmd), &cgdcontrdp_match, 1, + false); + if (ret < 0) { + LOG_ERR("Failed to send AT+CGCONTRDP command: %d", ret); + return; + } +} + +void dns_work_cb(void) +{ +#if defined(CONFIG_DNS_RESOLVER) && !defined(CONFIG_DNS_SERVER_IP_ADDRESSES) + int ret; + struct dns_resolve_context *dnsCtx; + struct sockaddr temp_addr; + bool valid_address = false; + bool retry = false; + const char *const dns_servers_str[DNS_SERVERS_COUNT] = { +#ifdef CONFIG_NET_IPV6 + socket_data.dns_v6_string, +#endif +#ifdef CONFIG_NET_IPV4 + socket_data.dns_v4_string, +#endif + NULL}; + const char *dns_servers_wrapped[ARRAY_SIZE(dns_servers_str)]; + +#ifdef CONFIG_NET_IPV6 + valid_address = net_ipaddr_parse(socket_data.dns_v6_string, + strlen(socket_data.dns_v6_string), &temp_addr); + if (!valid_address && IS_ENABLED(CONFIG_NET_IPV4)) { + /* IPv6 DNS string is not valid, replace it with IPv4 address and recheck */ + strncpy(socket_data.dns_v6_string, socket_data.dns_v4_string, + sizeof(socket_data.dns_v4_string) - 1); + valid_address = net_ipaddr_parse(socket_data.dns_v6_string, + strlen(socket_data.dns_v6_string), &temp_addr); + } +#else + valid_address = net_ipaddr_parse(socket_data.dns_v4_string, + strlen(socket_data.dns_v4_string), &temp_addr); +#endif + if (!valid_address) { + LOG_WRN("No valid DNS address!"); + return; + } + if (!socket_data.net_iface || !net_if_is_up(socket_data.net_iface) || + socket_data.dns_ready) { + return; + } + + memcpy(dns_servers_wrapped, dns_servers_str, sizeof(dns_servers_wrapped)); + + /* set new DNS addr in DNS resolver */ + LOG_DBG("Refresh DNS resolver"); + dnsCtx = dns_resolve_get_default(); + if (dnsCtx->state == DNS_RESOLVE_CONTEXT_INACTIVE) { + LOG_DBG("Initializing DNS resolver"); + ret = dns_resolve_init(dnsCtx, dns_servers_wrapped, NULL); + } else { + LOG_DBG("Reconfiguring DNS resolver"); + ret = dns_resolve_reconfigure(dnsCtx, dns_servers_wrapped, NULL); + } + if (ret < 0) { + LOG_ERR("dns_resolve_reconfigure fail (%d)", ret); + retry = true; + } else { + LOG_DBG("DNS ready"); + socket_data.dns_ready = true; + } + if (retry) { + LOG_WRN("DNS not ready, scheduling a retry"); + } +#endif +} + +static int on_cmd_sockread_common(int socket_id, int socket_data_length, uint16_t len, + void *user_data) +{ + struct modem_socket *sock; + struct socket_read_data *sock_data; + int ret = 0; + int packet_size = 0; + + sock = modem_socket_from_fd(&socket_data.socket_config, socket_id); + if (!sock) { + LOG_ERR("Socket not found! (%d)", socket_id); + return -EINVAL; + } + + sock_data = sock->data; + if (!sock_data) { + LOG_ERR("Socket data missing! Ignoring (%d)", socket_id); + return -EINVAL; + } + + if (!len || socket_data_length <= 0) { + LOG_ERR("Invalid data length: %d. Aborting!", socket_data_length); + return -EAGAIN; + } + + if (len < socket_data_length) { + LOG_DBG("Incomplete data received! Expected: %d, Received: %d", socket_data_length, + len); + return -EAGAIN; + } + + ret = ring_buf_get(socket_data.buf_pool, sock_data->recv_buf, len); + if (ret != len) { + LOG_ERR("Data retrieval mismatch: expected %zu, got %d", len, ret); + return -EAGAIN; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_HEXDUMP_DBG(sock_data->recv_buf, ret, "Received Data:"); +#endif + + if (sock_data->recv_buf_len < len) { + LOG_ERR("Buffer overflow! Received: %zu vs. Available: %zu", len, + sock_data->recv_buf_len); + return -EINVAL; + } + + sock_data->recv_read_len = len; + + if (sock_data->recv_read_len != socket_data_length) { + LOG_ERR("Data mismatch! Copied: %zu vs. Received: %d", len, socket_data_length); + return -EINVAL; + } + + /* Remove packet from list */ + packet_size = modem_socket_next_packet_size(&socket_data.socket_config, sock); + modem_socket_packet_size_update(&socket_data.socket_config, sock, -packet_size); + ring_buf_reset(socket_data.buf_pool); + socket_data.collected_buf_len = 0; + + return len; +} + +int modem_handle_data_capture(size_t target_len, struct hl78xx_data *data) +{ + return on_cmd_sockread_common(socket_data.current_sock_fd, socket_data.sizeof_socket_data, + target_len, data); +} + +/* + * generic socket creation function + * which can be called in bind() or connect() + */ +static int create_socket(struct modem_socket *sock, const struct sockaddr *addr, + struct hl78xx_data *data) +{ +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + int ret; + int af; + char cmd_buf[sizeof("AT+KTCPCFG=#,#,\"" MODEM_HL78XX_ADDRESS_FAMILY_FORMAT + "\",#####,,,,#,,#") + + NET_IPV6_ADDR_LEN]; + char ip_str[NET_IPV6_ADDR_LEN]; + uint16_t dst_port = 0U; + bool udp_configproto = false; + uint8_t mode = 0; + + memcpy(&sock->dst, addr, sizeof(*addr)); + if (addr->sa_family == AF_INET6) { + dst_port = ntohs(net_sin6(addr)->sin6_port); + af = MDM_HL78XX_SOCKET_AF_IPV6; + } else if (addr->sa_family == AF_INET) { + dst_port = ntohs(net_sin(addr)->sin_port); + af = MDM_HL78XX_SOCKET_AF_IPV4; + } else { + errno = EAFNOSUPPORT; + return -1; + } + + if (sock->ip_proto == IPPROTO_UDP) { + udp_configproto = true; + } + + 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; + } + if (!udp_configproto) { + snprintk(cmd_buf, sizeof(cmd_buf), "AT+KTCPCFG=%d,%d,\"%s\",%u,,,,%d,,%d", 1U, 0U, + ip_str, dst_port, af, 0U); + ret = modem_cmd_send_int(data, NULL, cmd_buf, strlen(cmd_buf), &ktcp_match, 1, + false); + if (ret < 0) { + goto error; + } + } else { + + uint8_t display_data_urc = 2; + +#if defined(CONFIG_MODEM_HL78XX_SOCKET_UDP_DISPLAY_DATA_URC) + display_data_urc = CONFIG_MODEM_HL78XX_SOCKET_UDP_DISPLAY_DATA_URC; +#endif + snprintk(cmd_buf, sizeof(cmd_buf), "AT+KUDPCFG=1,%u,,%d,,,%d,%d", mode, + display_data_urc, (addr->sa_family - 1), 0); + + ret = modem_cmd_send_int(data, NULL, cmd_buf, strlen(cmd_buf), &kudpind_match, 1, + false); + if (ret < 0) { + goto error; + } + } + errno = 0; + return 0; +error: + LOG_ERR("%s ret:%d", cmd_buf, ret); + modem_socket_put(&socket_data.socket_config, sock->sock_fd); + errno = -ret; + return -1; +} + +/* + * Socket Offload OPS + */ + +static int offload_socket(int family, int type, int proto) +{ + int ret; + /* defer modem's socket create call to bind() */ + ret = modem_socket_get(&socket_data.socket_config, family, type, proto); + if (ret < 0) { + errno = -ret; + return -1; + } + + errno = 0; + return ret; +} + +static int offload_close(void *obj) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + char buf[sizeof("AT+KTCPCLOSE=##\r")]; + int ret; + + if (!hl78xx_is_registered(socket_data.mdata_global)) { + LOG_ERR("Modem currently not attached to the network!"); + return -EAGAIN; + } + /* make sure socket is allocated and assigned an id */ + if (modem_socket_id_is_assigned(&socket_data.socket_config, sock) == false) { + return 0; + } + + if (sock->is_connected) { + if (sock->ip_proto == IPPROTO_UDP) { + snprintk(buf, sizeof(buf), "AT+KUDPCLOSE=%d", sock->id); + + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), + NULL, 0, false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + } + } else { + snprintk(buf, sizeof(buf), "AT+KTCPCLOSE=%d", sock->id); + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), + NULL, 0, false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + } + } + sock->is_connected = false; + } + + modem_socket_put(&socket_data.socket_config, sock->sock_fd); + /* Consider here successfully socket is closed */ + hl78xx_delegate_event(socket_data.mdata_global, MODEM_HL78XX_EVENT_SCRIPT_SUCCESS); + return 0; +} + +static int offload_bind(void *obj, const struct sockaddr *addr, socklen_t addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + /* Save bind address information */ + memcpy(&sock->src, addr, sizeof(*addr)); + /* Check if socket is allocated */ + if (modem_socket_is_allocated(&socket_data.socket_config, sock)) { + /* Trigger socket creation */ + if (create_socket(sock, addr, socket_data.mdata_global) < 0) { + LOG_ERR("%d %s SOCKET CREATION", __LINE__, __func__); + return -1; + } + } + return 0; +} + +static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + int ret = 0; + int af; + char buf[sizeof("AT+KTCPCFG=#,#,\"" MODEM_HL78XX_ADDRESS_FAMILY_FORMAT + "\",#####,,,,#,,#\r")]; + char ip_str[NET_IPV6_ADDR_LEN]; + uint16_t dst_port = 0U; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + if (!addr) { + errno = EINVAL; + return -1; + } + if (!hl78xx_is_registered(socket_data.mdata_global)) { + errno = ENETUNREACH; + 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; + } + + /* make sure socket has been allocated */ + if (modem_socket_is_allocated(&socket_data.socket_config, sock) == false) { + LOG_ERR("Invalid socket_id(%d) from fd:%d", sock->id, sock->sock_fd); + errno = EINVAL; + return -1; + } + + /* make sure we've created the socket */ + if (modem_socket_id_is_assigned(&socket_data.socket_config, sock) == false && + sock->ip_proto == IPPROTO_UDP) { + LOG_DBG("%d %s no socket assigned", __LINE__, __func__); + if (create_socket(sock, addr, socket_data.mdata_global) < 0) { + return -1; + } + } + + memcpy(&sock->dst, addr, sizeof(*addr)); + if (addr->sa_family == AF_INET6) { + af = MDM_HL78XX_SOCKET_AF_IPV6; + dst_port = ntohs(net_sin6(addr)->sin6_port); + } else if (addr->sa_family == AF_INET) { + af = MDM_HL78XX_SOCKET_AF_IPV4; + dst_port = ntohs(net_sin(addr)->sin_port); + } else { + errno = EAFNOSUPPORT; + return -1; + } + /* skip socket connect if UDP */ + if (sock->ip_proto == IPPROTO_UDP) { + errno = 0; + return 0; + } + ret = modem_context_sprint_ip_addr(addr, ip_str, sizeof(ip_str)); + if (ret != 0) { + errno = -ret; + LOG_ERR("Error formatting IP string %d", ret); + return -1; + } + snprintk(buf, sizeof(buf), "AT+KTCPCFG=%d,%d,\"%s\",%u,,,,%d,,%d", 1U, 0U, ip_str, dst_port, + af, 0U); + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), &ktcp_match, 1, + false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + errno = -ret; + return -1; + } + + snprintk(buf, sizeof(buf), "AT+KTCPCNX=%d", sock->id); + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), &ok_match, 1, + false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + errno = -ret; + return -1; + } + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, "", 0, &ktcpind_match, 1, false); + if (ret) { + LOG_ERR("Error sending data %d", ret); + ret = -ETIMEDOUT; + LOG_ERR("%d No TCP_IND received, ret: %d", __LINE__, ret); + return -1; + } + sock->is_connected = true; + errno = 0; + return 0; +} + +static bool validate_recv_args(void *buf, size_t len, int flags) +{ + if (!buf || len == 0) { + errno = EINVAL; + return false; + } + if (flags & ZSOCK_MSG_PEEK) { + errno = ENOTSUP; + return false; + } + return true; +} + +static int lock_socket_mutex(void) +{ + int ret = k_mutex_lock(&socket_data.mdata_global->tx_lock, K_SECONDS(1)); + + if (ret < 0) { + errno = -ret; + } + return ret; +} + +static int wait_for_data_if_needed(struct modem_socket *sock, int flags) +{ + int size = modem_socket_next_packet_size(&socket_data.socket_config, sock); + + if (size > 0) { + return size; + } + + if (flags & ZSOCK_MSG_DONTWAIT) { + errno = EAGAIN; + return -1; + } + + if (!sock->is_connected && sock->ip_proto != IPPROTO_UDP) { + errno = 0; + return 0; + } + + modem_socket_wait_data(&socket_data.socket_config, sock); + return modem_socket_next_packet_size(&socket_data.socket_config, sock); +} + +static void prepare_read_command(char *sendbuf, size_t bufsize, struct modem_socket *sock, + size_t read_size) +{ + snprintk(sendbuf, bufsize, "AT+K%sRCV=%d,%zd%s", + sock->ip_proto == IPPROTO_UDP ? "UDP" : "TCP", sock->id, read_size, + socket_data.mdata_global->chat.delimiter); +} + +static void setup_socket_data(struct modem_socket *sock, struct socket_read_data *sock_data, + void *buf, size_t len, struct sockaddr *from, uint16_t read_size) +{ + memset(sock_data, 0, sizeof(*sock_data)); + sock_data->recv_buf = buf; + sock_data->recv_buf_len = len; + sock_data->recv_addr = from; + sock->data = sock_data; + + socket_data.sizeof_socket_data = read_size; + socket_data.requested_socket_id = sock->id; + socket_data.current_sock_fd = sock->sock_fd; + socket_data.expected_buf_len = read_size + sizeof("\r\n") - 1 + + socket_data.mdata_global->buffers.eof_pattern_size + + sizeof(MODEM_STREAM_END_WORD) - 1; +} + +static void restore_socket_state(void) +{ + k_mutex_unlock(&socket_data.mdata_global->tx_lock); + modem_chat_attach(&socket_data.mdata_global->chat, socket_data.mdata_global->uart_pipe); + k_work_submit(&socket_data.mdata_global->chat.receive_work); + socket_data.expected_buf_len = 0; +} + +static void check_tcp_state_if_needed(struct modem_socket *sock) +{ + if (atomic_test_and_clear_bit(&state_leftover, MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT) && + sock->ip_proto == IPPROTO_TCP) { + const char *check_ktcp_stat = "AT+KTCPSTAT"; + + modem_cmd_send_int(socket_data.mdata_global, NULL, check_ktcp_stat, + strlen(check_ktcp_stat), &ktcp_state_match, 1, true); + } +} + +static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, struct sockaddr *from, + socklen_t *fromlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + int ret; + char sendbuf[sizeof("AT+KUDPRCV=#,##########\r\n")]; + struct socket_read_data sock_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + + if (!validate_recv_args(buf, len, flags)) { + return -1; + } + + ret = lock_socket_mutex(); + if (ret < 0) { + return -1; + } + + int next_packet_size = wait_for_data_if_needed(sock, flags); + + if (next_packet_size <= 0) { + ret = next_packet_size; + goto exit; + } + + uint32_t max_data_length = + MDM_MAX_DATA_LENGTH - (socket_data.mdata_global->buffers.eof_pattern_size + + sizeof(MODEM_STREAM_STARTER_WORD) - 1); + next_packet_size = MIN(next_packet_size, max_data_length); + uint16_t read_size = MIN(next_packet_size, len); + + prepare_read_command(sendbuf, sizeof(sendbuf), sock, read_size); + setup_socket_data(sock, &sock_data, buf, len, from, read_size); + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s socket_fd: %d, socket_id: %d, expected_data_len: %d", __LINE__, __func__, + socket_data.current_sock_fd, socket_data.requested_socket_id, + socket_data.expected_buf_len); + LOG_HEXDUMP_DBG(sendbuf, strlen(sendbuf), "sending"); +#endif + + modem_chat_release(&socket_data.mdata_global->chat); + modem_pipe_attach(socket_data.mdata_global->uart_pipe, modem_pipe_callback, + socket_data.mdata_global); + + if (k_sem_take(&socket_data.mdata_global->script_stopped_sem_tx_int, K_FOREVER) < 0 || + modem_pipe_transmit(socket_data.mdata_global->uart_pipe, sendbuf, strlen(sendbuf)) < + 0 || + k_sem_take(&socket_data.mdata_global->script_stopped_sem_rx_int, K_FOREVER) < 0 || + modem_handle_data_capture(read_size, socket_data.mdata_global) < 0) { + ret = -1; + goto exit; + } + + if (from && fromlen) { + *fromlen = sizeof(sock->dst); + memcpy(from, &sock->dst, *fromlen); + } + + errno = 0; + ret = sock_data.recv_read_len; + +exit: +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %d %d", __LINE__, __func__, errno, ret); +#endif + restore_socket_state(); + check_tcp_state_if_needed(sock); + return ret; +} + +static int validate_socket(const struct modem_socket *sock) +{ + + if (!sock) { + return -EINVAL; + } + + if (!sock->is_connected && sock->ip_proto != IPPROTO_UDP) { + errno = ENOTCONN; + return -1; + } + + return 0; +} + +int check_if_any_socket_connected(void) +{ + struct modem_socket_config *cfg = &socket_data.socket_config; + + k_sem_take(&cfg->sem_lock, K_FOREVER); + + for (int i = 0; i < cfg->sockets_len; i++) { + if (cfg->sockets[i].is_connected) { + /* if there is any socket connected*/ + return true; + } + } + + k_sem_give(&cfg->sem_lock); + return false; +} + +static int prepare_udp_send_cmd(const struct modem_socket *sock, const struct sockaddr *dst_addr, + size_t buf_len, char *cmd_buf, size_t cmd_buf_size) +{ + char ip_str[NET_IPV6_ADDR_LEN]; + uint16_t dst_port = 0; + int ret = 0; + + ret = modem_context_sprint_ip_addr(dst_addr, ip_str, sizeof(ip_str)); + if (ret < 0) { + LOG_ERR("Error formatting IP string %d", ret); + return ret; + } + + ret = modem_context_get_addr_port(dst_addr, &dst_port); + if (ret < 0) { + LOG_ERR("Error getting port from IP address %d", ret); + return ret; + } + + snprintk(cmd_buf, cmd_buf_size, "AT+KUDPSND=%d,\"%s\",%u,%zu", sock->id, ip_str, dst_port, + buf_len); + return 0; +} + +static int prepare_tcp_send_cmd(const struct modem_socket *sock, size_t buf_len, char *cmd_buf, + size_t cmd_buf_size) +{ + snprintk(cmd_buf, cmd_buf_size, "AT+KTCPSND=%d,%zu", sock->id, buf_len); + return 0; +} + +static int send_data_buffer(const char *buf, const size_t buf_len, int *sock_written) +{ + uint32_t offset = 0; + int len = buf_len; + int ret = 0; + + if (len == 0) { + LOG_DBG("%d %s No data to send", __LINE__, __func__); + return 0; + } + + while (len > 0) { + if (k_sem_take(&socket_data.mdata_global->script_stopped_sem_tx_int, K_FOREVER) < + 0) { + return -1; + } + + ret = modem_pipe_transmit(socket_data.mdata_global->uart_pipe, + ((const uint8_t *)buf) + offset, len); + if (ret <= 0) { + LOG_ERR("Transmit error %d", ret); + return -1; + } + + offset += ret; + len -= ret; + *sock_written += ret; + } + return 0; +} + +/* send binary data via the +KUDPSND/+KTCPSND commands */ +static ssize_t send_socket_data(void *obj, const struct sockaddr *dst_addr, const char *buf, + size_t buf_len, k_timeout_t timeout) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + char cmd_buf[82] = {0}; /* AT+KUDPSND/KTCP=,IP,PORT,LENGTH */ + int ret; + int sock_written = 0; + + ret = validate_socket(sock); + if (ret < 0) { + return ret; + } + + if (!dst_addr && sock->ip_proto == IPPROTO_UDP) { + dst_addr = &sock->dst; + } + + if (buf_len > MDM_MAX_DATA_LENGTH) { + if (sock->type == SOCK_DGRAM) { + errno = EMSGSIZE; + return -1; + } + buf_len = MDM_MAX_DATA_LENGTH; + } + + if (sock->ip_proto == IPPROTO_UDP) { + ret = prepare_udp_send_cmd(sock, dst_addr, buf_len, cmd_buf, sizeof(cmd_buf)); + } else { + ret = prepare_tcp_send_cmd(sock, buf_len, cmd_buf, sizeof(cmd_buf)); + } + if (ret < 0) { + return -1; + } + + if (k_mutex_lock(&socket_data.mdata_global->tx_lock, K_SECONDS(1)) < 0) { + errno = -ret; + return -1; + } + + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, cmd_buf, strlen(cmd_buf), + connect_matches, ARRAY_SIZE(connect_matches), false); + if (ret < 0) { + LOG_ERR("Error sending AT command %d", ret); + goto cleanup; + } + + modem_pipe_attach(socket_data.mdata_global->chat.pipe, modem_pipe_callback, + &socket_data.mdata_global->chat); + + ret = send_data_buffer(buf, buf_len, &sock_written); + if (ret < 0) { + goto cleanup; + } + + ret = k_sem_take(&socket_data.mdata_global->script_stopped_sem_tx_int, K_FOREVER); + if (ret < 0) { + goto cleanup; + } + + ret = modem_pipe_transmit(socket_data.mdata_global->uart_pipe, + (uint8_t *)socket_data.mdata_global->buffers.eof_pattern, + socket_data.mdata_global->buffers.eof_pattern_size); + if (ret < 0) { + LOG_ERR("Error sending EOF pattern: %d", ret); + } + + modem_chat_attach(&socket_data.mdata_global->chat, socket_data.mdata_global->uart_pipe); + + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, "", 0, &ok_match, 1, false); + if (ret < 0) { + LOG_ERR("Final confirmation failed: %d", ret); + goto cleanup; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %d %d", __LINE__, __func__, sock_written, ret); +#endif +cleanup: + k_mutex_unlock(&socket_data.mdata_global->tx_lock); + return (ret < 0) ? -1 : sock_written; +} + +static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags, + const struct sockaddr *to, socklen_t tolen) +{ + int ret = 0; + struct modem_socket *sock = (struct modem_socket *)obj; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + if (!hl78xx_is_registered(socket_data.mdata_global)) { + LOG_ERR("Modem currently not attached to the network!"); + return -EAGAIN; + } + /* 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; + } + /* Only send up to MTU bytes. */ + if (len > MDM_MAX_DATA_LENGTH) { + len = MDM_MAX_DATA_LENGTH; + } + + ret = send_socket_data(obj, to, buf, len, K_SECONDS(MDM_CMD_TIMEOUT)); + if (ret < 0) { + errno = -ret; + return -1; + } + + errno = 0; + return ret; +} + +static int offload_ioctl(void *obj, unsigned int request, va_list args) +{ + int ret = 0; + struct modem_socket *sock = (struct modem_socket *)obj; +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %d", __LINE__, __func__, request); +#endif + switch (request) { + case ZFD_IOCTL_POLL_PREPARE: { + struct zsock_pollfd *pfd; + struct k_poll_event **pev; + struct k_poll_event *pev_end; + + pfd = va_arg(args, struct zsock_pollfd *); + pev = va_arg(args, struct k_poll_event **); + pev_end = va_arg(args, struct k_poll_event *); + LOG_DBG("poll_prepare: fd=%d, events=0x%x", pfd->fd, pfd->events); + + ret = modem_socket_poll_prepare(&socket_data.socket_config, obj, pfd, pev, pev_end); + + if (ret == -1 && errno == ENOTSUP && (pfd->events & ZSOCK_POLLOUT) && + sock->ip_proto == IPPROTO_UDP) { + /* Not Implemented */ + /* + * You can implement this later when needed + * For now, just ignore it + */ + errno = ENOTSUP; + ret = 0; + } + return ret; + } + case ZFD_IOCTL_POLL_UPDATE: { + struct zsock_pollfd *pfd; + struct k_poll_event **pev; + + pfd = va_arg(args, struct zsock_pollfd *); + pev = va_arg(args, struct k_poll_event **); + LOG_DBG("poll_update: fd=%d", pfd->fd); + return modem_socket_poll_update(obj, pfd, pev); + } + + case F_GETFL: + return 0; + case F_SETFL: { + int flags = va_arg(args, int); + + LOG_DBG("F_SETFL called with flags=0x%x", flags); + /* You can store flags if you want, but it's safe to just ignore them. */ + return 0; + } + default: + errno = EINVAL; + return -1; + } +} + +static ssize_t offload_read(void *obj, void *buffer, size_t count) +{ + return offload_recvfrom(obj, buffer, count, 0, NULL, 0); +} + +static ssize_t offload_write(void *obj, const void *buffer, size_t count) +{ + return offload_sendto(obj, buffer, count, 0, NULL, 0); +} + +static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags) +{ + ssize_t sent = 0; + struct iovec bkp_iovec = {0}; + struct msghdr crafted_msg = {.msg_name = msg->msg_name, .msg_namelen = msg->msg_namelen}; + size_t full_len = 0; + int ret; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + /* Compute the full length to send and validate input */ + for (int i = 0; i < msg->msg_iovlen; i++) { + if (!msg->msg_iov[i].iov_base || msg->msg_iov[i].iov_len == 0) { + errno = EINVAL; + return -1; + } + full_len += msg->msg_iov[i].iov_len; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("msg_iovlen:%zd flags:%d, full_len:%zd", msg->msg_iovlen, flags, full_len); +#endif + while (full_len > sent) { + int removed = 0; + int i = 0; + int bkp_iovec_idx = -1; + + crafted_msg.msg_iovlen = msg->msg_iovlen; + crafted_msg.msg_iov = &msg->msg_iov[0]; + + /* Adjust iovec to remove already sent bytes */ + while (removed < sent) { + int to_remove = sent - removed; + + if (to_remove >= msg->msg_iov[i].iov_len) { + crafted_msg.msg_iovlen -= 1; + crafted_msg.msg_iov = &msg->msg_iov[i + 1]; + removed += msg->msg_iov[i].iov_len; + } else { + bkp_iovec_idx = i; + bkp_iovec = msg->msg_iov[i]; + + msg->msg_iov[i].iov_len -= to_remove; + msg->msg_iov[i].iov_base = + ((uint8_t *)msg->msg_iov[i].iov_base) + to_remove; + + removed += to_remove; + } + i++; + } + + ret = send_socket_data(obj, crafted_msg.msg_name, crafted_msg.msg_iov->iov_base, + crafted_msg.msg_iovlen, K_SECONDS(MDM_CMD_TIMEOUT)); + + if (bkp_iovec_idx != -1) { + msg->msg_iov[bkp_iovec_idx] = bkp_iovec; + } + + if (ret < 0) { + errno = -ret; + return -1; + } + + sent += ret; + } + + return sent; +} +/* clang-format off */ +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 = offload_bind, + .connect = offload_connect, + .sendto = offload_sendto, + .recvfrom = offload_recvfrom, + .listen = NULL, + .accept = NULL, + .sendmsg = offload_sendmsg, + .getsockopt = NULL, + .setsockopt = NULL, +}; +/* clang-format on */ +static int hl78xx_init_sockets(const struct device *dev) +{ + int ret; + + socket_data.buf_pool = &mdm_recv_pool; +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + /* socket config */ + ret = modem_socket_init(&socket_data.socket_config, &socket_data.sockets[0], + ARRAY_SIZE(socket_data.sockets), MDM_BASE_SOCKET_NUM, false, + &offload_socket_fd_op_vtable); + if (ret) { + goto error; + } + return 0; +error: + return ret; +} + +void socknotifydata(int socket_id, int new_total) +{ + struct modem_socket *sock; + int ret = 0; + + sock = modem_socket_from_id(&socket_data.socket_config, socket_id); + if (!sock) { + return; + } + LOG_DBG("%d new total: %d sckid%d", __LINE__, new_total, socket_id); + ret = modem_socket_packet_size_update(&socket_data.socket_config, sock, new_total); + if (ret < 0) { + LOG_ERR("socket_id:%d left_bytes:%d err: %d", socket_id, new_total, ret); + } + if (new_total > 0) { + modem_socket_data_ready(&socket_data.socket_config, sock); + } +} + +void hl78xx_socket_init(struct hl78xx_data *data) +{ +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + socket_data.mdata_global = data; + atomic_set(&state_leftover, 0); +} + +static void modem_net_iface_init(struct net_if *iface) +{ + const struct device *dev = net_if_get_device(iface); + struct hl78xx_socket_data *data = dev->data; +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s", __LINE__, __func__); +#endif + net_if_set_link_addr( + iface, modem_get_mac(socket_data.mac_addr, socket_data.mdata_global->identity.imei), + sizeof(data->mac_addr), NET_LINK_ETHERNET); + data->net_iface = iface; + + hl78xx_init_sockets(dev); + net_if_socket_offload_set(iface, offload_socket); +} + +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) +{ + return (family == AF_INET || family == AF_INET6) && + (type == SOCK_DGRAM || type == SOCK_STREAM) && + (proto == IPPROTO_TCP || proto == IPPROTO_UDP); +} + +#define MODEM_HL78XX_DEFINE_INSTANCE(inst) \ + NET_DEVICE_OFFLOAD_INIT(inst, "hl78xx_dev", NULL, NULL, &socket_data, NULL, \ + CONFIG_MODEM_HL78XX_OFFLOAD_INIT_PRIORITY, &api_funcs, \ + MDM_MAX_DATA_LENGTH); \ + NET_SOCKET_OFFLOAD_REGISTER(inst, CONFIG_NET_SOCKETS_OFFLOAD_PRIORITY, AF_UNSPEC, \ + offload_is_supported, offload_socket); + +#define MODEM_DEVICE_SWIR_HL78XX(inst) MODEM_HL78XX_DEFINE_INSTANCE(inst) + +#define DT_DRV_COMPAT swir_hl7812 +DT_INST_FOREACH_STATUS_OKAY(MODEM_DEVICE_SWIR_HL78XX) +#undef DT_DRV_COMPAT + +#define DT_DRV_COMPAT swir_hl7800 +DT_INST_FOREACH_STATUS_OKAY(MODEM_DEVICE_SWIR_HL78XX) +#undef DT_DRV_COMPAT diff --git a/drivers/modem/hl78xx/hl78xx_utility.c b/drivers/modem/hl78xx/hl78xx_utility.c new file mode 100644 index 000000000000..b6c749274275 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_utility.c @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "hl78xx.h" + +#define ICCID_PREFIX_LEN 7 +#define IMSI_PREFIX_LEN 6 +#define MAX_BANDS 32 + +LOG_MODULE_REGISTER(hl78xx_utility, CONFIG_MODEM_LOG_LEVEL); + +/** + * @brief Convert string to long integer, but handle errors + * + * @param s: string with representation of integer number + * @param err_value: on error return this value instead + * @param desc: name the string being converted + * @param func: function where this is called (typically __func__) + * + * @retval return integer conversion on success, or err_value on error + */ +int modem_atoi(const char *s, const int err_value, const char *desc, const char *func) +{ + int ret; + char *endptr; + + ret = (int)strtol(s, &endptr, 10); + if (!endptr || *endptr != '\0') { + LOG_ERR("bad %s '%s' in %s", s, desc, func); + return err_value; + } + + return ret; +} + +bool hl78xx_is_registered(struct hl78xx_data *data) +{ + return (data->status.registration.network_state == HL78XX_REGISTRATION_REGISTERED_HOME) || + (data->status.registration.network_state == HL78XX_REGISTRATION_REGISTERED_ROAMING); +} + +#define HASH_MULTIPLIER 37 +uint32_t hash32(const char *str, int len) +{ + uint32_t h = 0; + + for (int i = 0; i < len; ++i) { + h = (h * HASH_MULTIPLIER) + str[i]; + } + + return h; +} + +/** + * Portable memmem() replacement for C99. + */ +const void *c99_memmem(const void *haystack, size_t haystacklen, const void *needle, + size_t needlelen) +{ + if (!haystack || !needle || needlelen == 0 || haystacklen < needlelen) { + return NULL; + } + + const uint8_t *h = haystack; + + for (size_t i = 0; i <= haystacklen - needlelen; i++) { + if (memcmp(h + i, needle, needlelen) == 0) { + return (h + i); + } + } + + return NULL; +} + +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_ICCID) || defined(CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI) +int find_apn(const char *profile, const char *associated_number, char *apn_buff, uint8_t prefix_len) +{ + char buffer[512]; + char *saveptr; + + if (prefix_len > strlen(associated_number)) { + return -1; + } + + strncpy(buffer, profile, sizeof(buffer) - 1); + buffer[sizeof(buffer) - 1] = '\0'; + + char *token = strtok_r(buffer, ",", &saveptr); + + while (token != NULL) { + char *equal_sign = strchr(token, '='); + + if (equal_sign != NULL) { + *equal_sign = '\0'; + char *p_apn = token; + char *associated_number_prefix = equal_sign + 1; + + /* Trim leading whitespace */ + while (*p_apn == ' ') { + p_apn++; + } + while (*associated_number_prefix == ' ') { + associated_number_prefix++; + } + + if (strncmp(associated_number, associated_number_prefix, prefix_len) == 0) { + strncpy(apn_buff, p_apn, MDM_APN_MAX_LENGTH - 1); + apn_buff[MDM_APN_MAX_LENGTH - 1] = '\0'; + return 0; + } + } + + token = strtok_r(NULL, ",", &saveptr); + } + + /* No match found, clear apn_buff */ + apn_buff[0] = '\0'; + return -1; /* not found */ +} + +/* try to detect APN automatically, based on IMSI / ICCID */ +int modem_detect_apn(struct hl78xx_data *data, const char *associated_number) +{ + int rc = -1; + + if (associated_number != NULL && strlen(associated_number) >= 5) { +/* extract MMC and MNC from IMSI */ +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI) + /* + * First 5 digits (e.g. 31026) → often sufficient to identify carrier. + * However, in some regions (like the US), MNCs can be 3 digits (e.g. 310260). + */ + char mmcmnc[7] = {0}; /* IMSI */ +#define APN_PREFIX_LEN IMSI_PREFIX_LEN +#else + /* These 7 digits are generally sufficient to identify the SIM provider. + */ + char mmcmnc[8] = {0}; /* ICCID */ +#define APN_PREFIX_LEN ICCID_PREFIX_LEN +#endif + + strncpy(mmcmnc, associated_number, sizeof(mmcmnc) - 1); + mmcmnc[sizeof(mmcmnc) - 1] = '\0'; + /* try to find a matching IMSI/ICCID, and assign the APN */ + rc = find_apn(CONFIG_MODEM_HL78XX_APN_PROFILES, mmcmnc, data->identity.apn, + APN_PREFIX_LEN); + if (rc < 0) { + LOG_ERR("%d %s APN Parser error %d", __LINE__, __func__, rc); + } + } + + if (rc == 0) { + LOG_INF("Assign APN: \"%s\"", data->identity.apn); + } else { + LOG_INF("No assigned APN: \"%d\"", rc); + } + + return rc; +} +#endif + +void set_band_bit(uint8_t *bitmap, uint16_t band_num) +{ + if (band_num < 1 || band_num > 256) { + return; /* Out of range */ + } + + uint16_t bit_pos = band_num - 1; + uint16_t byte_index = bit_pos / 8; + uint8_t bit_index = bit_pos % 8; + /* Big-endian format: band 1 in byte 31, band 256 in byte 0 */ + bitmap[byte_index] |= (1 << bit_index); +} + +static uint8_t hl78xx_generate_band_bitmap(uint8_t *bitmap) +{ + memset(bitmap, 0, MDM_BAND_BITMAP_LEN_BYTES); + /* Index is reversed: Band 1 is LSB of byte 31, Band 256 is MSB of byte 0 */ + +#if CONFIG_MODEM_HL78XX_BAND_1 + set_band_bit(bitmap, 1); +#endif +#if CONFIG_MODEM_HL78XX_BAND_2 + set_band_bit(bitmap, 2); +#endif +#if CONFIG_MODEM_HL78XX_BAND_3 + set_band_bit(bitmap, 3); +#endif +#if CONFIG_MODEM_HL78XX_BAND_4 + set_band_bit(bitmap, 4); +#endif +#if CONFIG_MODEM_HL78XX_BAND_5 + set_band_bit(bitmap, 5); +#endif +#if CONFIG_MODEM_HL78XX_BAND_8 + set_band_bit(bitmap, 8); +#endif +#if CONFIG_MODEM_HL78XX_BAND_9 + set_band_bit(bitmap, 9); +#endif +#if CONFIG_MODEM_HL78XX_BAND_10 + set_band_bit(bitmap, 10); +#endif +#if CONFIG_MODEM_HL78XX_BAND_12 + set_band_bit(bitmap, 12); +#endif +#if CONFIG_MODEM_HL78XX_BAND_13 + set_band_bit(bitmap, 13); +#endif +#if CONFIG_MODEM_HL78XX_BAND_17 + set_band_bit(bitmap, 17); +#endif +#if CONFIG_MODEM_HL78XX_BAND_18 + set_band_bit(bitmap, 18); +#endif +#if CONFIG_MODEM_HL78XX_BAND_19 + set_band_bit(bitmap, 19); +#endif +#if CONFIG_MODEM_HL78XX_BAND_20 + set_band_bit(bitmap, 20); +#endif +#if CONFIG_MODEM_HL78XX_BAND_23 + set_band_bit(bitmap, 23); +#endif +#if CONFIG_MODEM_HL78XX_BAND_25 + set_band_bit(bitmap, 25); +#endif +#if CONFIG_MODEM_HL78XX_BAND_26 + set_band_bit(bitmap, 26); +#endif +#if CONFIG_MODEM_HL78XX_BAND_27 + set_band_bit(bitmap, 27); +#endif +#if CONFIG_MODEM_HL78XX_BAND_28 + set_band_bit(bitmap, 28); +#endif +#if CONFIG_MODEM_HL78XX_BAND_31 + set_band_bit(bitmap, 31); +#endif +#if CONFIG_MODEM_HL78XX_BAND_66 + set_band_bit(bitmap, 66); +#endif +#if CONFIG_MODEM_HL78XX_BAND_72 + set_band_bit(bitmap, 72); +#endif +#if CONFIG_MODEM_HL78XX_BAND_73 + set_band_bit(bitmap, 73); +#endif +#if CONFIG_MODEM_HL78XX_BAND_85 + set_band_bit(bitmap, 85); +#endif +#if CONFIG_MODEM_HL78XX_BAND_87 + set_band_bit(bitmap, 87); +#endif +#if CONFIG_MODEM_HL78XX_BAND_88 + set_band_bit(bitmap, 88); +#endif +#if CONFIG_MODEM_HL78XX_BAND_106 + set_band_bit(bitmap, 106); +#endif +#if CONFIG_MODEM_HL78XX_BAND_107 + set_band_bit(bitmap, 107); +#endif +#if CONFIG_MODEM_HL78XX_BAND_255 + set_band_bit(bitmap, 255); +#endif +#if CONFIG_MODEM_HL78XX_BAND_256 + set_band_bit(bitmap, 256); +#endif + /* Add additional bands similarly... */ + + return 0; +} +#if defined(CONFIG_MODEM_HL78XX_AUTORAT) +/** + * @brief Parse a comma-separated list of bands from a string. + * + * @param band_str The input string containing band numbers. + * @param bands Output array to store parsed band numbers. + * @param max_bands Maximum number of bands that can be stored in the output array. + * + * @return Number of bands parsed, or negative error code on failure. + */ +static int parse_band_list(const char *band_str, int *bands, size_t max_bands) +{ + if (!band_str || !bands || max_bands == 0) { + return -EINVAL; + } + + char buf[128] = {0}; + + strncpy(buf, band_str, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + char *token; + char *rest = buf; + int count = 0; + + while ((token = strtok_r(rest, ",", &rest))) { + int band = atoi(token); + + if (band <= 0) { + printk("Invalid band number: %s\n", token); + continue; + } + if (count >= max_bands) { + printk("Too many bands, max is %d\n", (int)max_bands); + break; + } + bands[count++] = band; + } + + return count; +} +#endif /* CONFIG_MODEM_HL78XX_AUTORAT */ + +int hl78xx_generate_bitmap_from_config(enum hl78xx_cell_rat_mode rat, uint8_t *bitmap_out) +{ + if (!bitmap_out) { + return -EINVAL; + } + + memset(bitmap_out, 0, MDM_BAND_BITMAP_LEN_BYTES); + +#if defined(CONFIG_MODEM_HL78XX_AUTORAT) + /* Auto-RAT: read bands from string configs */ + const char *band_str = NULL; + + switch (rat) { + case HL78XX_RAT_CAT_M1: +#ifdef CONFIG_MODEM_HL78XX_AUTORAT_M1_BAND_CFG + band_str = CONFIG_MODEM_HL78XX_AUTORAT_M1_BAND_CFG; +#endif + break; + case HL78XX_RAT_NB1: +#ifdef CONFIG_MODEM_HL78XX_AUTORAT_NB_BAND_CFG + band_str = CONFIG_MODEM_HL78XX_AUTORAT_NB_BAND_CFG; +#endif + break; + default: + return -EINVAL; + } + + if (band_str) { + int bands[MAX_BANDS]; + int count = parse_band_list(band_str, bands, MAX_BANDS); + + if (count < 0) { + return -EINVAL; + } + + for (int i = 0; i < count; i++) { + set_band_bit(bitmap_out, bands[i]); + } + return 0; + } +#endif /* CONFIG_MODEM_HL78XX_AUTORAT */ + + /* Else: use standalone config */ + return hl78xx_generate_band_bitmap(bitmap_out); +} +void hl78xx_bitmap_to_hex_string_trimmed(const uint8_t *bitmap, char *hex_str, size_t hex_str_len) +{ + + int started = 0; + size_t offset = 0; + + for (int i = MDM_BAND_BITMAP_LEN_BYTES - 1; i >= 0; i--) { + if (!started && bitmap[i] == 0) { + continue; /* Skip leading zero bytes */ + } + + started = 1; + + if (offset + 2 >= hex_str_len) { + break; + } + + offset += snprintk(&hex_str[offset], hex_str_len - offset, "%02X", bitmap[i]); + } + + if (!started) { + strcpy(hex_str, "0"); + } +} + +int hl78xx_hex_string_to_bitmap(const char *hex_str, uint8_t *bitmap_out) +{ + if (strlen(hex_str) >= MDM_BAND_HEX_STR_LEN) { + LOG_ERR("Invalid hex string length: %zu", strlen(hex_str)); + return -EINVAL; + } + + for (int i = 0; i < MDM_BAND_BITMAP_LEN_BYTES; i++) { + unsigned int byte_val; + + if (sscanf(&hex_str[i * 2], "%2x", &byte_val) != 1) { + LOG_ERR("Failed to parse byte at position %d", i); + return -EINVAL; + } + bitmap_out[i] = (uint8_t)byte_val; + } + + return 0; +} + +int hl78xx_get_band_default_config_for_rat(enum hl78xx_cell_rat_mode rat, char *hex_bndcfg, + size_t size_in_bytes) +{ + uint8_t bitmap[MDM_BAND_BITMAP_LEN_BYTES] = {0}; + char hex_str[MDM_BAND_HEX_STR_LEN] = {0}; + + if (size_in_bytes < MDM_BAND_HEX_STR_LEN || hex_bndcfg == NULL) { + return -EINVAL; + } + + if (hl78xx_generate_bitmap_from_config(rat, bitmap) != 0) { + return -EINVAL; + } + + hl78xx_bitmap_to_hex_string_trimmed(bitmap, hex_str, sizeof(hex_str)); + + LOG_INF("Default band config: %s", hex_str); + + strncpy(hex_bndcfg, hex_str, MDM_BAND_HEX_STR_LEN); + return 0; +} + +const char *hl78xx_trim_leading_zeros(const char *hex_str) +{ + while (*hex_str == '0' && *(hex_str + 1) != '\0') { + hex_str++; + } + return hex_str; +} + +static void strip_quotes(char *str) +{ + size_t len = strlen(str); + + if (len >= 2 && str[0] == '"' && str[len - 1] == '"') { + /* Shift string left by 1 and null-terminate earlier */ + memmove(str, str + 1, len - 2); + str[len - 2] = '\0'; + } +} + +void hl78xx_extract_essential_part_apn(const char *full_apn, char *essential_apn, size_t max_len) +{ + + char apn_buf[max_len]; + + strncpy(apn_buf, full_apn, sizeof(apn_buf) - 1); + apn_buf[sizeof(apn_buf) - 1] = '\0'; + /* Remove surrounding quotes if any */ + strip_quotes(apn_buf); + + const char *mnc_ptr = strstr(apn_buf, ".mnc"); + size_t len; + + if (mnc_ptr != NULL) { + len = mnc_ptr - apn_buf; + if (len >= max_len) { + len = max_len - 1; + } + strncpy(essential_apn, apn_buf, len); + essential_apn[len] = '\0'; + } else { + /* No ".mnc" found, copy entire string */ + strncpy(essential_apn, apn_buf, max_len - 1); + essential_apn[max_len - 1] = '\0'; + } +} diff --git a/include/zephyr/drivers/modem/hl78xx_apis.h b/include/zephyr/drivers/modem/hl78xx_apis.h new file mode 100644 index 000000000000..083b1f84b1d4 --- /dev/null +++ b/include/zephyr/drivers/modem/hl78xx_apis.h @@ -0,0 +1,557 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef ZEPHYR_INCLUDE_DRIVERS_HL78XX_APIS_H_ +#define ZEPHYR_INCLUDE_DRIVERS_HL78XX_APIS_H_ + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 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)) +/** Monitor is paused. */ +#define PAUSED 1 +/** Monitor is active, default */ +#define ACTIVE 0 + +/** + * @brief Define an Event monitor to receive notifications in the system workqueue thread. + * + * @param name The monitor name. + * @param _handler The monitor callback. + * @param ... Optional monitor initial state (@c PAUSED or @c ACTIVE). + * The default initial state of a monitor is active. + */ +#define HL78XX_EVT_MONITOR(name, _handler, ...) \ + static void _handler(struct hl78xx_evt *); \ + static STRUCT_SECTION_ITERABLE(hl78xx_evt_monitor_entry, name) = { \ + .handler = _handler, \ + .flags.direct = false, \ + COND_CODE_1(__VA_ARGS__, (.flags.paused = __VA_ARGS__,), ()) } + +/** Cellular radio access technologies */ +enum hl78xx_cell_rat_mode { + HL78XX_RAT_CAT_M1 = 0, + HL78XX_RAT_NB1, +#ifdef CONFIG_MODEM_HL7812 + HL78XX_RAT_GSM, +#ifdef CONFIG_MODEM_FW_R6 + HL78XX_RAT_NBNTN, +#endif +#endif +#ifdef CONFIG_MODEM_HL78XX_AUTORAT + HL78XX_RAT_MODE_AUTO, +#endif + HL78XX_RAT_MODE_NONE, + HL78XX_RAT_COUNT = HL78XX_RAT_MODE_NONE +}; +/* */ +enum hl78xx_phone_functionality { + HL78XX_SIM_POWER_OFF, + HL78XX_FULLY_FUNCTIONAL, + HL78XX_AIRPLANE = 4, +}; + +/** Cellular network structure */ +struct hl78xx_network { + /** Cellular access technology */ + enum hl78xx_cell_rat_mode technology; + /** + * List of bands, as defined by the specified cellular access technology, + * to enables. All supported bands are enabled if none are provided. + */ + uint16_t *bands; + /** Size of bands */ + uint16_t size; +}; + +/** Cellular signal type */ +enum hl78xx_signal_type { + HL78XX_SIGNAL_RSSI, + HL78XX_SIGNAL_RSRP, + HL78XX_SIGNAL_RSRQ, +}; + +/** Cellular modem info type */ +enum hl78xx_modem_info_type { + /** International Mobile Equipment Identity */ + HL78XX_MODEM_INFO_IMEI, + /** Modem model ID */ + HL78XX_MODEM_INFO_MODEL_ID, + /** Modem manufacturer */ + HL78XX_MODEM_INFO_MANUFACTURER, + /** Modem fw version */ + HL78XX_MODEM_INFO_FW_VERSION, + /** International Mobile Subscriber Identity */ + HL78XX_MODEM_INFO_SIM_IMSI, + /** Integrated Circuit Card Identification Number (SIM) */ + HL78XX_MODEM_INFO_SIM_ICCID, + /* Access Point Name */ + HL78XX_MODEM_INFO_APN, +}; + +enum hl78xx_registration_status { + HL78XX_REGISTRATION_NOT_REGISTERED = 0, + HL78XX_REGISTRATION_REGISTERED_HOME, + HL78XX_REGISTRATION_SEARCHING, + HL78XX_REGISTRATION_DENIED, + HL78XX_REGISTRATION_UNKNOWN, + HL78XX_REGISTRATION_REGISTERED_ROAMING, +}; + +enum hl78xx_evt_type { + HL78XX_RAT_UPDATE, + HL78XX_LTE_REGISTRATION_STAT_UPDATE, + HL78XX_LTE_SIM_REGISTRATION, + HL78XX_LTE_PSMEV, +}; + +struct hl78xx_evt { + enum hl78xx_evt_type type; + + union { + enum hl78xx_registration_status reg_status; + enum hl78xx_cell_rat_mode rat_mode; + } content; +}; +/** API for configuring networks */ +typedef int (*hl78xx_api_configure_networks)(const struct device *dev, + const struct hl78xx_network *networks, uint8_t size); + +/** API for getting supported networks */ +typedef int (*hl78xx_api_get_supported_networks)(const struct device *dev, + const struct hl78xx_network **networks, + uint8_t *size); + +/** API for getting network signal strength */ +typedef int (*hl78xx_api_get_signal)(const struct device *dev, const enum hl78xx_signal_type type, + int16_t *value); + +/** API for getting modem information */ +typedef int (*hl78xx_api_get_modem_info)(const struct device *dev, + const enum hl78xx_modem_info_type type, char *info, + size_t size); + +/** API for getting registration status */ +typedef int (*hl78xx_api_get_registration_status)(const struct device *dev, + enum hl78xx_cell_rat_mode *tech, + enum hl78xx_registration_status *status); + +/** API for setting apn */ +typedef int (*hl78xx_api_set_apn)(const struct device *dev, const char *apn, uint16_t size); + +/** API for set phone functionality */ +typedef int (*hl78xx_api_set_phone_functionality)(const struct device *dev, + enum hl78xx_phone_functionality functionality, + bool reset); + +/** API for get phone functionality */ +typedef int (*hl78xx_api_get_phone_functionality)(const struct device *dev, + enum hl78xx_phone_functionality *functionality); + +/** API for get phone functionality */ +typedef int (*hl78xx_api_send_at_cmd)(const struct device *dev, const char *cmd, uint16_t cmd_size, + const struct modem_chat_match *response_matches, + uint16_t matches_size); + +typedef void (*hl78xx_evt_monitor_handler_t)(struct hl78xx_evt *notif); + +struct hl78xx_evt_monitor_entry { + /** Monitor callback. */ + const hl78xx_evt_monitor_handler_t handler; + struct { + uint8_t paused: 1; /* Monitor is paused. */ + uint8_t direct: 1; /* Dispatch in ISR. */ + } flags; +}; + +/** Cellular driver API */ +__subsystem struct hl78xx_driver_api { + hl78xx_api_configure_networks configure_networks; + hl78xx_api_get_supported_networks get_supported_networks; + hl78xx_api_get_signal get_signal; + hl78xx_api_get_modem_info get_modem_info; + hl78xx_api_get_registration_status get_registration_status; + hl78xx_api_set_apn set_apn; + hl78xx_api_set_phone_functionality set_phone_functionality; + hl78xx_api_get_phone_functionality get_phone_functionality; + hl78xx_api_send_at_cmd send_at_cmd; +}; + +/** + * @brief Configure cellular networks for the device + * + * @details Cellular network devices support at least one cellular access technology. + * Each cellular access technology defines a set of bands, of which the cellular device + * will support all or a subset of. + * + * The cellular device can only use one cellular network technology at a time. It must + * exclusively use the cellular network configurations provided, and will prioritize + * the cellular network configurations in the order they are provided in case there are + * multiple (the first cellular network configuration has the highest priority). + * + * @param dev Cellular network device instance. + * @param networks List of cellular network configurations to apply. + * @param size Size of list of cellular network configurations. + * + * @retval 0 if successful. + * @retval -EINVAL if any provided cellular network configuration is invalid or unsupported. + * @retval -ENOTSUP if API is not supported by cellular network device. + * @retval Negative errno-code otherwise. + */ +static inline int hl78xx_configure_networks(const struct device *dev, + const struct hl78xx_network *networks, uint8_t size) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->configure_networks == NULL) { + return -ENOSYS; + } + + return api->configure_networks(dev, networks, size); +} + +/** + * @brief Get supported cellular networks for the device + * + * @param dev Cellular network device instance + * @param networks Pointer to list of supported cellular network configurations. + * @param size Size of list of cellular network configurations. + * + * @retval 0 if successful. + * @retval -ENOTSUP if API is not supported by cellular network device. + * @retval Negative errno-code otherwise. + */ +static inline int hl78xx_get_supported_networks(const struct device *dev, + const struct hl78xx_network **networks, + uint8_t *size) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->get_supported_networks == NULL) { + return -ENOSYS; + } + + return api->get_supported_networks(dev, networks, size); +} + +/** + * @brief Get signal for the device + * + * @param dev Cellular network device instance + * @param type Type of the signal information requested + * @param value Signal strength destination (one of RSSI, RSRP, RSRQ) + * + * @retval 0 if successful. + * @retval -ENOTSUP if API is not supported by cellular network device. + * @retval -ENODATA if device is not in a state where signal can be polled + * @retval Negative errno-code otherwise. + */ +static inline int hl78xx_get_signal(const struct device *dev, const enum hl78xx_signal_type type, + int16_t *value) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->get_signal == NULL) { + return -ENOSYS; + } + + return api->get_signal(dev, type, value); +} + +/** + * @brief Get modem info for the device + * + * @param dev Cellular network device instance + * @param type Type of the modem info requested + * @param info Info string destination + * @param size Info string size + * + * @retval 0 if successful. + * @retval -ENOTSUP if API is not supported by cellular network device. + * @retval -ENODATA if modem does not provide info requested + * @retval Negative errno-code from chat module otherwise. + */ +static inline int hl78xx_get_modem_info(const struct device *dev, + const enum hl78xx_modem_info_type type, char *info, + size_t size) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->get_modem_info == NULL) { + return -ENOSYS; + } + + return api->get_modem_info(dev, type, info, size); +} + +/** + * @brief Get network registration status for the device + * + * @param dev Cellular network device instance + * @param tech Which access technology to get status for + * @param status Registration status for given access technology + * + * @retval 0 if successful. + * @retval -ENOSYS if API is not supported by cellular network device. + * @retval -ENODATA if modem does not provide info requested + * @retval Negative errno-code from chat module otherwise. + */ +static inline int hl78xx_get_registration_status(const struct device *dev, + enum hl78xx_cell_rat_mode *tech, + enum hl78xx_registration_status *status) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->get_registration_status == NULL) { + return -ENOSYS; + } + + return api->get_registration_status(dev, tech, status); +} + +/** + * @brief Set the Access Point Name (APN) for the modem. + * + * Stores the specified APN string in the modem driver context to be used + * during PDP context activation or network registration. + * + * @param dev Pointer to the modem device instance. + * @param apn Null-terminated string representing the APN to be set. + * @param size Length of the APN string, including the null terminator. + * + * @retval 0 on success. + * @retval -EINVAL if input parameters are invalid. + */ +static inline int hl78xx_set_apn(const struct device *dev, const char *apn, uint16_t size) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->set_apn == NULL) { + return -ENOSYS; + } + + return api->set_apn(dev, apn, size); +} +/** + * @brief Set the modem phone functionality mode. + * + * Configures the operational state of the modem (e.g., full, airplane, or minimum functionality). + * Optionally, the modem can be reset during this transition. + * + * @param dev Pointer to the modem device instance. + * @param functionality Desired phone functionality mode to be set. + * (e.g., full, airplane, minimum – see enum hl78xx_phone_functionality) + * @param reset If true, the modem will be reset as part of applying the functionality change. + * + * @retval 0 on success. + * @retval -EINVAL if an invalid parameter is passed. + * @retval -EIO on communication or command failure with the modem. + */ +static inline int hl78xx_set_phone_functionality(const struct device *dev, + enum hl78xx_phone_functionality functionality, + bool reset) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->set_phone_functionality == NULL) { + return -ENOSYS; + } + + return api->set_phone_functionality(dev, functionality, reset); +} +/** + * @brief Get the current phone functionality mode of the modem. + * + * Queries the modem to retrieve its current operational mode, such as + * full functionality, airplane mode, or minimum functionality. + * + * @param dev Pointer to the modem device instance. + * @param functionality Pointer to store the retrieved functionality mode. + * (see enum hl78xx_phone_functionality) + * + * @retval 0 on success. + * @retval -EINVAL if the input parameters are invalid. + * @retval -EIO if the modem fails to respond or returns an error. + */ +static inline int hl78xx_get_phone_functionality(const struct device *dev, + enum hl78xx_phone_functionality *functionality) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->get_phone_functionality == NULL) { + return -ENOSYS; + } + + return api->get_phone_functionality(dev, functionality); +} +/** + * @brief Send an AT command to the modem and wait for a matched response. + * + * Transmits the specified AT command to the modem and waits for a response that matches + * one of the expected patterns defined in the response match table. + * + * @param dev Pointer to the modem device instance. + * @param cmd Pointer to the AT command string to be sent. + * @param cmd_size Length of the AT command string in bytes. + * @param response_matches Pointer to an array of expected response patterns. + * (see struct modem_chat_match) + * @param matches_size Number of response patterns in the array. + * + * @retval 0 on successful command transmission and response match. + * @retval -EINVAL if any parameter is invalid. + * @retval -ETIMEDOUT if the modem did not respond in the expected time. + * @retval -EIO on communication failure or if response did not match. + */ +static inline int hl78xx_modem_cmd_send(const struct device *dev, const char *cmd, + uint16_t cmd_size, + const struct modem_chat_match *response_matches, + uint16_t matches_size) +{ + const struct hl78xx_driver_api *api = (const struct hl78xx_driver_api *)dev->api; + + if (api->send_at_cmd == NULL) { + return -ENOSYS; + } + + return api->send_at_cmd(dev, cmd, cmd_size, response_matches, matches_size); +} +/** + * @brief Convert raw RSSI value from the modem to dBm. + * + * Parses the RSSI value reported by the modem (typically from an AT command response) + * and converts it to a corresponding signal strength in dBm, as defined by 3GPP TS 27.007. + * + * @param rssi Raw RSSI value (0–31 or 99 for not known or not detectable). + * @param value Pointer to store the converted RSSI in dBm. + * + * @retval 0 on successful conversion. + * @retval -EINVAL if the RSSI value is out of valid range or unsupported. + */ +static inline int hl78xx_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; +} +/** + * @brief Convert raw RSRP value from the modem to dBm. + * + * Parses the Reference Signal Received Power (RSRP) value reported by the modem + * and converts it into a corresponding signal strength in dBm, typically based on + * 3GPP TS 36.133 specifications. + * + * @param rsrp Raw RSRP value (commonly in the range 0–97, or 255 if unknown). + * @param value Pointer to store the converted RSRP in dBm. + * + * @retval 0 on successful conversion. + * @retval -EINVAL if the RSRP value is out of range or represents an unknown value. + */ +static inline int hl78xx_parse_rsrp(uint8_t rsrp, int16_t *value) +{ + /* AT+CESQ returns a response + * +CESQ: ,,,,, where: + * rsrq is a integer from 0 to 34 whose values describes the Reference + * Signal Receive Quality between -20 dB for 0 and -3 dB for 34 + * (0.5 dB steps), or unknown for 255 + * rsrp is an integer from 0 to 97 that describes the Reference Signal + * Receive Power between -140 dBm for 0 and -44 dBm for 97 (1 dBm steps), + * or unknown for 255 + */ + if (rsrp == CESQ_RSRP_UNKNOWN) { + return -EINVAL; + } + + *value = (int16_t)CESQ_RSRP_TO_DB(rsrp); + return 0; +} +/** + * @brief Convert raw RSRQ value from the modem to dB. + * + * Parses the Reference Signal Received Quality (RSRQ) value provided by the modem + * and converts it into a signal quality measurement in decibels (dB), as specified + * by 3GPP TS 36.133. + * + * @param rsrq Raw RSRQ value (typically 0–34, or 255 if unknown). + * @param value Pointer to store the converted RSRQ in dB. + * + * @retval 0 on successful conversion. + * @retval -EINVAL if the RSRQ value is out of valid range or indicates unknown. + */ +static inline int hl78xx_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; +} +/** + * @brief Pause monitor. + * + * Pause monitor @p mon from receiving notifications. + * + * @param mon The monitor to pause. + */ +static inline void hl78xx_evt_monitor_pause(struct hl78xx_evt_monitor_entry *mon) +{ + mon->flags.paused = true; +} + +/** + * @brief Resume monitor. + * + * Resume forwarding notifications to monitor @p mon. + * + * @param mon The monitor to resume. + */ +static inline void hl78xx_evt_monitor_resume(struct hl78xx_evt_monitor_entry *mon) +{ + mon->flags.paused = false; +} + +/** + * @brief Set the event notification handler for HL78xx modem events. + * + * Registers a callback handler to receive asynchronous event notifications + * from the HL78xx modem, such as network registration changes, GNSS updates, + * or other modem-generated events. + * + * @param handler Function pointer to the event monitor callback. + * Pass NULL to clear the existing handler. + * + * @retval 0 on success. + * @retval -EINVAL if the handler parameter is invalid. + */ +int hl78xx_evt_notif_handler_set(hl78xx_evt_monitor_handler_t handler); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_DRIVERS_HL78XX_APIS_H_ */ From deb43c5943810977d6d7c61be62b1924ddd6170c Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Sun, 8 Jun 2025 22:23:11 +0100 Subject: [PATCH 2/7] samples: drivers: modem: hello_hl78xx sample Add HL78xx driver sample application Signed-off-by: Zafer SEN --- samples/drivers/modem/hello_hl78xx/.gitignore | 6 + .../drivers/modem/hello_hl78xx/CMakeLists.txt | 13 + samples/drivers/modem/hello_hl78xx/Kconfig | 7 + samples/drivers/modem/hello_hl78xx/README.rst | 54 ++++ samples/drivers/modem/hello_hl78xx/prj.conf | 81 ++++++ .../drivers/modem/hello_hl78xx/sample.yaml | 16 ++ samples/drivers/modem/hello_hl78xx/src/main.c | 238 ++++++++++++++++++ samples/drivers/modem/index.rst | 5 + 8 files changed, 420 insertions(+) create mode 100644 samples/drivers/modem/hello_hl78xx/.gitignore create mode 100644 samples/drivers/modem/hello_hl78xx/CMakeLists.txt create mode 100644 samples/drivers/modem/hello_hl78xx/Kconfig create mode 100644 samples/drivers/modem/hello_hl78xx/README.rst create mode 100644 samples/drivers/modem/hello_hl78xx/prj.conf create mode 100644 samples/drivers/modem/hello_hl78xx/sample.yaml create mode 100644 samples/drivers/modem/hello_hl78xx/src/main.c create mode 100644 samples/drivers/modem/index.rst diff --git a/samples/drivers/modem/hello_hl78xx/.gitignore b/samples/drivers/modem/hello_hl78xx/.gitignore new file mode 100644 index 000000000000..635a99b7955a --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/.gitignore @@ -0,0 +1,6 @@ +# editors +*.swp +*~ + +# build +/build*/ diff --git a/samples/drivers/modem/hello_hl78xx/CMakeLists.txt b/samples/drivers/modem/hello_hl78xx/CMakeLists.txt new file mode 100644 index 000000000000..ab5ef99b1c4b --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/CMakeLists.txt @@ -0,0 +1,13 @@ +# Sierra Wireless HL78XX driver driver options + +# Copyright (c) 2025 Netfeasa +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(hello_hl78xx) + +target_sources(app PRIVATE src/main.c) + +include(${ZEPHYR_BASE}/samples/net/common/common.cmake) diff --git a/samples/drivers/modem/hello_hl78xx/Kconfig b/samples/drivers/modem/hello_hl78xx/Kconfig new file mode 100644 index 000000000000..12cacd7b37a6 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/Kconfig @@ -0,0 +1,7 @@ +# Sierra Wireless HL78XX driver driver options + +# Copyright (c) 2025 Netfeasa +# SPDX-License-Identifier: Apache-2.0 + +source "samples/net/common/Kconfig" +source "Kconfig.zephyr" diff --git a/samples/drivers/modem/hello_hl78xx/README.rst b/samples/drivers/modem/hello_hl78xx/README.rst new file mode 100644 index 000000000000..f7110c8ecafa --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/README.rst @@ -0,0 +1,54 @@ +.. zephyr:code-sample:: hello_hl78xx + :name: Hello hl78xx modem driver + + get & set basic hl78xx modem information & functionality with HL78XX modem APIs + +Overview +******** + +A simple sample that can be used with only Sierra Wireles HL78XX series modems + +Notes +***** + +This sample uses the devicetree alias ``modem`` to identify +the modem instance to use. + +Building and Running +******************** + +This application can be built and executed on QEMU as follows: + +.. zephyr-app-commands:: + :zephyr-app: samples/drivers/modem/hello_hl78xx + :host-os: all + :goals: build flash + :compact: + +To build for another board, change "qemu_x86" above to that board's name. + +Sample Output +============= + +.. code-block:: console + + ********************************************************** + ********* Hello HL78XX Modem Sample Application ********** + ********************************************************** + [00:00:16.881,000] main: Manufacturer: Sierra Wireless + [00:00:16.881,000] main: Firmware Version: HL7812.5.5.17.0 + [00:00:16.881,000] main: APN: netfeasavod + [00:00:16.881,000] main: Imei: 352244440111111 + [00:00:16.881,000] main: RAT: NB1 + [00:00:16.881,000] main: Connection status: Roaming Network + [00:00:16.881,000] main: RSRP : -90 + ********************************************************** + +After startup, code performs: + +#. Modem readiness check and power-on +#. Network interface setup via Zephyr's Connection Manager +#. Modem queries (manufacturer, firmware, APN, IMEI, signal strength, etc.) +#. Network registration and signal strength checks +#. Setting and verifying a new APN +#. Sending an AT command to validate communication diff --git a/samples/drivers/modem/hello_hl78xx/prj.conf b/samples/drivers/modem/hello_hl78xx/prj.conf new file mode 100644 index 000000000000..e1325db0bcbb --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/prj.conf @@ -0,0 +1,81 @@ +# Sierra Wireless HL78XX driver driver options + +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +# The HL78xx driver gets its IP settings from the cell network + +#system +CONFIG_HEAP_MEM_POOL_SIZE=4096 +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 +CONFIG_POSIX_API=y + +#PM +# CONFIG_PM_DEVICE=y + +#uart +CONFIG_UART_ASYNC_API=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# Generic networking options +CONFIG_NETWORKING=y +CONFIG_NET_UDP=y +CONFIG_NET_TCP=y +CONFIG_NET_IPV6=n +CONFIG_NET_IPV4=y +CONFIG_NET_SOCKETS=y + +# DNS +CONFIG_DNS_RESOLVER=y +CONFIG_NET_SOCKETS_DNS_TIMEOUT=5000 + +# Wait for the network to be ready +CONFIG_NET_SAMPLE_COMMON_WAIT_DNS_SERVER_ADDITION=y + +# Network management +CONFIG_NET_MGMT=y +CONFIG_NET_MGMT_EVENT=y +# NB-IoT has large latency, so increase timeouts. It is ok to use this for Cat-M1 as well. +CONFIG_NET_SOCKETS_CONNECT_TIMEOUT=15000 +CONFIG_NET_CONNECTION_MANAGER=y + +# Network buffers +CONFIG_NET_PKT_RX_COUNT=32 +CONFIG_NET_PKT_TX_COUNT=16 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=32 + +# Modem driver +CONFIG_MODEM=y + +#hl78xx modem +CONFIG_MODEM_HL78XX=y +CONFIG_MODEM_HL7812=y + +# Statistics +CONFIG_MODEM_STATS=y +CONFIG_SHELL=y + +#apn source +# CONFIG_MODEM_HL78XX_APN_SOURCE_KCONFIG=y +# CONFIG_MODEM_HL78XX_APN="internet" + +CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE=y + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y + +# Logging +CONFIG_LOG=y +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BUFFER_SIZE=32768 +# For extra verbosity +# CONFIG_MODEM_MODULES_LOG_LEVEL_DBG=y +# CONFIG_MODEM_LOG_LEVEL_DBG=y +# CONFIG_MODEM_CHAT_LOG_BUFFER_SIZE=1024 +# CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG=y diff --git a/samples/drivers/modem/hello_hl78xx/sample.yaml b/samples/drivers/modem/hello_hl78xx/sample.yaml new file mode 100644 index 000000000000..625cf998cd74 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/sample.yaml @@ -0,0 +1,16 @@ +sample: + description: Sample for HL78XX modem + name: Hello HL78XX sample +common: + tags: + - modem + - hl78xx + filter: dt_alias_exists("modem") +tests: + sample.driver.modem.hello_hl78xx: + platform_allow: + - nucleo_u575zi_q + integration_platforms: + - nucleo_u575zi_q + extra_args: + - SHIELD=swir_hl78xx_ev_kit diff --git a/samples/drivers/modem/hello_hl78xx/src/main.c b/samples/drivers/modem/hello_hl78xx/src/main.c new file mode 100644 index 000000000000..1dd997776c73 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/src/main.c @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2025 Netfeasa + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/* Macros used to subscribe to specific Zephyr NET management events. */ +#if defined(CONFIG_NET_SAMPLE_COMMON_WAIT_DNS_SERVER_ADDITION) +#define L4_EVENT_MASK (NET_EVENT_DNS_SERVER_ADD | NET_EVENT_L4_DISCONNECTED) +#else +#define L4_EVENT_MASK (NET_EVENT_L4_CONNECTED | NET_EVENT_L4_DISCONNECTED) +#endif +#define CONN_LAYER_EVENT_MASK (NET_EVENT_CONN_IF_FATAL_ERROR) + +LOG_MODULE_REGISTER(main, CONFIG_MODEM_LOG_LEVEL); + +static K_SEM_DEFINE(network_connected_sem, 0, 1); +const struct device *modem = DEVICE_DT_GET(DT_ALIAS(modem)); + +/* Zephyr NET management event callback structures. */ +static struct net_mgmt_event_callback l4_cb; +static struct net_mgmt_event_callback conn_cb; + +static const char *rat_get_in_string(enum hl78xx_cell_rat_mode rat) +{ + switch (rat) { + case HL78XX_RAT_CAT_M1: + return "CAT-M1"; + case HL78XX_RAT_NB1: + return "NB1"; + case HL78XX_RAT_GSM: + return "GSM"; +#ifdef CONFIG_MODEM_FW_R6 + case HL78XX_RAT_NBNTN: + return "NTN"; +#endif + default: + return "Not ready"; + } +} + +static const char *reg_status_get_in_string(enum hl78xx_registration_status rat) +{ + switch (rat) { + case HL78XX_REGISTRATION_NOT_REGISTERED: + return "Not Registered"; + case HL78XX_REGISTRATION_REGISTERED_HOME: + return "Home Network"; + case HL78XX_REGISTRATION_SEARCHING: + return "Network Searching"; + case HL78XX_REGISTRATION_DENIED: + return "Registiration Denied"; + case HL78XX_REGISTRATION_UNKNOWN: + return "Out of covarege or Unknown"; + case HL78XX_REGISTRATION_REGISTERED_ROAMING: + return "Roaming Network"; + default: + return "Not ready"; + } +} +/* Zephyr NET management event callback structures. */ +static void on_net_event_l4_disconnected(void) +{ + LOG_INF("Disconnected from network"); +} + +static void on_net_event_l4_connected(void) +{ + LOG_INF("Connected to network"); + k_sem_give(&network_connected_sem); +} + +static void connectivity_event_handler(struct net_mgmt_event_callback *cb, uint64_t event, + struct net_if *iface) +{ + if (event == NET_EVENT_CONN_IF_FATAL_ERROR) { + LOG_ERR("Fatal error received from the connectivity layer"); + return; + } +} + +static void l4_event_handler(struct net_mgmt_event_callback *cb, uint64_t event, + struct net_if *iface) +{ + switch (event) { +#if defined(CONFIG_NET_SAMPLE_COMMON_WAIT_DNS_SERVER_ADDITION) + case NET_EVENT_DNS_SERVER_ADD: +#else + case NET_EVENT_L4_CONNECTED: +#endif + LOG_INF("IP Up"); + on_net_event_l4_connected(); + break; + case NET_EVENT_L4_DISCONNECTED: + LOG_INF("IP down"); + on_net_event_l4_disconnected(); + break; + default: + break; + } +} + +static void evnt_listener(struct hl78xx_evt *event) +{ +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d HL78XX modem Event Received: %d", __LINE__, event->type); +#endif + switch (event->type) { + /* Do something */ + case HL78XX_RAT_UPDATE: + LOG_DBG("%d HL78XX modem rat mode changed: %d", __LINE__, event->content.rat_mode); + break; + case HL78XX_LTE_REGISTRATION_STAT_UPDATE: + LOG_DBG("%d HL78XX modem registration status: %d", __LINE__, + event->content.reg_status); + break; + case HL78XX_LTE_SIM_REGISTRATION: + break; + case HL78XX_LTE_PSMEV: + break; + default: + break; + } +} + +static void hl78xx_on_ok(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %s %s", __LINE__, __func__, argv[0]); +#endif +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", hl78xx_on_ok); + +HL78XX_EVT_MONITOR(listner_evt, evnt_listener); + +int main(void) +{ + int ret = 0; + + if (device_is_ready(modem) == false) { + LOG_ERR("%d, %s Device %s is not ready", __LINE__, __func__, modem->name); + } +#ifdef CONFIG_PM_DEVICE + LOG_INF("Powering on modem\n"); + pm_device_action_run(modem, PM_DEVICE_ACTION_RESUME); +#endif + +#ifdef CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE + if (IS_ENABLED(CONFIG_NET_CONNECTION_MANAGER)) { + struct net_if *iface = net_if_get_default(); + + if (!iface) { + LOG_ERR("No network interface found!"); + return -ENODEV; + } + + /* Setup handler for Zephyr NET Connection Manager events. */ + net_mgmt_init_event_callback(&l4_cb, l4_event_handler, L4_EVENT_MASK); + net_mgmt_add_event_callback(&l4_cb); + + /* Setup handler for Zephyr NET Connection Manager Connectivity layer. */ + net_mgmt_init_event_callback(&conn_cb, connectivity_event_handler, + CONN_LAYER_EVENT_MASK); + net_mgmt_add_event_callback(&conn_cb); + + ret = net_if_up(iface); + + if (ret < 0 && ret != -EALREADY) { + LOG_ERR("net_if_up, error: %d", ret); + return ret; + } + + (void)conn_mgr_if_connect(iface); + + LOG_INF("Waiting for network connection..."); + k_sem_take(&network_connected_sem, K_FOREVER); + } +#endif + /* Start pleacing your modem based code here */ + char manufacturer[20] = {0}; + char fw_ver[17] = {0}; + char apn[64] = {0}; + char imei[17] = {0}; + enum hl78xx_cell_rat_mode tech; + enum hl78xx_registration_status status; + int16_t rsrp; + const char *newapn = ""; + const char *sample_cmd = "AT"; + + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_MANUFACTURER, manufacturer, + sizeof(manufacturer)); + + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_FW_VERSION, fw_ver, sizeof(fw_ver)); + + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_APN, apn, sizeof(apn)); + + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_IMEI, imei, sizeof(imei)); + + hl78xx_get_registration_status(modem, &tech, &status); + + hl78xx_get_signal(modem, HL78XX_SIGNAL_RSRP, &rsrp); + + LOG_RAW("\n**********************************************************\n"); + LOG_RAW("********* Hello HL78XX Modem Sample Application **********\n"); + LOG_RAW("**********************************************************\n"); + LOG_INF("Manufacturer: %s", manufacturer); + LOG_INF("Firmware Version: %s", fw_ver); + LOG_INF("APN: %s", apn); + LOG_INF("Imei: %s", imei); + LOG_INF("RAT: %s", rat_get_in_string(tech)); + LOG_INF("Connection status: %s", reg_status_get_in_string(status)); + LOG_INF("RSRP : %d", rsrp); + LOG_RAW("**********************************************************\n\n"); + + LOG_INF("Setting new APN: %s", newapn); + hl78xx_set_apn(modem, newapn, 0); + + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_APN, apn, sizeof(apn)); + + hl78xx_modem_cmd_send(modem, sample_cmd, strlen(sample_cmd), &ok_match, 1); + LOG_INF("New APN: %s", (strlen(apn) > 0) ? apn : "\"\""); + return 0; +} diff --git a/samples/drivers/modem/index.rst b/samples/drivers/modem/index.rst new file mode 100644 index 000000000000..5b7a92c1018f --- /dev/null +++ b/samples/drivers/modem/index.rst @@ -0,0 +1,5 @@ +.. zephyr:code-sample-category:: modem + :name: Modem + :show-listing: + + These samples demonstrate how to use the custom modem driver APIs. From 9dbdf3e1e1abacdea3be8f42a64097be34cd1b33 Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Sun, 8 Jun 2025 22:41:19 +0100 Subject: [PATCH 3/7] samples: net: lwm2m_client: add hl78xx driver config file add support for HL78xx driver Signed-off-by: Zafer SEN --- samples/net/common/Kconfig | 2 +- .../overlay-swir_hl78xx_ev_kit.conf | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 samples/net/lwm2m_client/overlay-swir_hl78xx_ev_kit.conf diff --git a/samples/net/common/Kconfig b/samples/net/common/Kconfig index afaab763ac58..ceed53efa869 100644 --- a/samples/net/common/Kconfig +++ b/samples/net/common/Kconfig @@ -6,7 +6,7 @@ config NET_SAMPLE_COMMON_WAIT_DNS_SERVER_ADDITION bool "Wait DNS server addition before considering connection to be up" - depends on MODEM_HL7800 && !DNS_SERVER_IP_ADDRESSES + depends on (MODEM_HL7800 || MODEM_HL78XX) && !DNS_SERVER_IP_ADDRESSES help Make sure we get DNS server addresses from the network before considering the connection to be up. diff --git a/samples/net/lwm2m_client/overlay-swir_hl78xx_ev_kit.conf b/samples/net/lwm2m_client/overlay-swir_hl78xx_ev_kit.conf new file mode 100644 index 000000000000..6be4db978bd4 --- /dev/null +++ b/samples/net/lwm2m_client/overlay-swir_hl78xx_ev_kit.conf @@ -0,0 +1,75 @@ +# Sierra Wireless HL78XX driver options + +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +# The HL78xx driver gets its IP settings from the cell network +CONFIG_NET_CONFIG_SETTINGS=n +CONFIG_NET_DHCPV4=n +CONFIG_DNS_SERVER_IP_ADDRESSES=n + +#PM +# CONFIG_PM_DEVICE=y + +#uart +CONFIG_UART_ASYNC_API=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# Generic networking options +CONFIG_NET_IPV6=n + +# DNS +CONFIG_DNS_RESOLVER=y +CONFIG_NET_SOCKETS_DNS_TIMEOUT=15000 + +# Wait for the network to be ready +CONFIG_NET_SAMPLE_LWM2M_WAIT_DNS=y + +# Network management +CONFIG_NET_MGMT=y +CONFIG_NET_MGMT_EVENT=y +CONFIG_NET_CONNECTION_MANAGER=y + +# NB-IoT has large latency, so increase timeouts. It is ok to use this for Cat-M1 as well. +CONFIG_NET_SOCKETS_CONNECT_TIMEOUT=15000 + +# Network buffers +CONFIG_NET_PKT_RX_COUNT=32 +CONFIG_NET_PKT_TX_COUNT=16 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=32 + +# Modem driver +CONFIG_MODEM=y + +#hl78xx modem +CONFIG_MODEM_HL78XX=y +CONFIG_MODEM_HL7812=y + +# Statistics +CONFIG_MODEM_STATS=y +CONFIG_SHELL=y +# Don't require device to have time/date +CONFIG_MBEDTLS_HAVE_TIME_DATE=n + +#apn source +# CONFIG_MODEM_HL78XX_APN_SOURCE_KCONFIG=y +# CONFIG_MODEM_HL78XX_APN="internet" + +CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE=y + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y + +# Logging +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BUFFER_SIZE=50000 +# For extra verbosity +CONFIG_MODEM_MODULES_LOG_LEVEL_DBG=y +CONFIG_MODEM_LOG_LEVEL_DBG=y +CONFIG_MODEM_CHAT_LOG_BUFFER_SIZE=1024 +CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG=y From 5c443de1322bb9285d0c0bdc16a0d725cd9d5b3c Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Sun, 8 Jun 2025 22:42:04 +0100 Subject: [PATCH 4/7] samples: net: cloud: aws_iot_mqtt: add hl78xx driver config file Add support for HL78xx driver Signed-off-by: Zafer SEN --- .../overlay-swir_hl78xx_ev_kit.conf | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx_ev_kit.conf diff --git a/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx_ev_kit.conf b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx_ev_kit.conf new file mode 100644 index 000000000000..83a447ac900b --- /dev/null +++ b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx_ev_kit.conf @@ -0,0 +1,77 @@ +# Sierra Wireless HL78XX driver options + +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +# The HL78xx driver gets its IP settings from the cell network +CONFIG_NET_CONFIG_SETTINGS=n +CONFIG_NET_DHCPV4=n +CONFIG_DNS_SERVER_IP_ADDRESSES=n + +#PM +# CONFIG_PM_DEVICE=y + +#uart +CONFIG_UART_ASYNC_API=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# Generic networking options +CONFIG_NET_IPV6=n + +# SNTP +CONFIG_NET_CONFIG_SNTP_INIT_SERVER="time.google.com" + +# DNS +CONFIG_NET_SOCKETS_DNS_TIMEOUT=15000 + +# Wait for the network to be ready +CONFIG_NET_SAMPLE_COMMON_WAIT_DNS_SERVER_ADDITION=y + +# Network management +CONFIG_NET_MGMT=y +CONFIG_NET_MGMT_EVENT=y +CONFIG_NET_CONNECTION_MANAGER=y + +# NB-IoT has large latency, so increase timeouts. It is ok to use this for Cat-M1 as well. +CONFIG_NET_SOCKETS_CONNECT_TIMEOUT=15000 + +# Network buffers +CONFIG_NET_PKT_RX_COUNT=32 +CONFIG_NET_PKT_TX_COUNT=16 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=32 + +# Modem driver +CONFIG_MODEM=y + +#hl78xx modem +CONFIG_MODEM_HL78XX=y +CONFIG_MODEM_HL7812=y + +# Statistics +CONFIG_MODEM_STATS=y +CONFIG_SHELL=y +# Don't require device to have time/date +CONFIG_MBEDTLS_HAVE_TIME_DATE=n + +#apn source +# CONFIG_MODEM_HL78XX_APN_SOURCE_KCONFIG=y +# CONFIG_MODEM_HL78XX_APN="internet" + +CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE=y + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y + +# Logging +# CONFIG_LOG_MODE_DEFERRED=y +# CONFIG_LOG_BUFFER_SIZE=50000 +# # For extra verbosity +# CONFIG_MODEM_MODULES_LOG_LEVEL_DBG=y +# CONFIG_MODEM_LOG_LEVEL_DBG=y +# CONFIG_MODEM_CHAT_LOG_BUFFER_SIZE=1024 +# CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG=y From dcaba7b526c54dc388cbf455b2d4c9a11379dd03 Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Sun, 8 Jun 2025 22:43:18 +0100 Subject: [PATCH 5/7] boards: shields: add swir_hl78xx_ev kit add support for HL78xx driver Signed-off-by: Zafer SEN --- .../shields/swir_hl78xx_ev_kit/Kconfig.shield | 5 ++ .../doc/img/SW-Dev-RC76.3.webp | Bin 0 -> 100678 bytes .../shields/swir_hl78xx_ev_kit/doc/index.rst | 79 ++++++++++++++++++ .../swir_hl78xx_ev_kit.overlay | 34 ++++++++ dts/bindings/modem/swir,hl7812.yaml | 8 ++ dts/bindings/modem/swir,hl78xx.yaml | 38 +++++++++ 6 files changed, 164 insertions(+) create mode 100644 boards/shields/swir_hl78xx_ev_kit/Kconfig.shield create mode 100644 boards/shields/swir_hl78xx_ev_kit/doc/img/SW-Dev-RC76.3.webp create mode 100644 boards/shields/swir_hl78xx_ev_kit/doc/index.rst create mode 100644 boards/shields/swir_hl78xx_ev_kit/swir_hl78xx_ev_kit.overlay create mode 100644 dts/bindings/modem/swir,hl7812.yaml create mode 100644 dts/bindings/modem/swir,hl78xx.yaml diff --git a/boards/shields/swir_hl78xx_ev_kit/Kconfig.shield b/boards/shields/swir_hl78xx_ev_kit/Kconfig.shield new file mode 100644 index 000000000000..e5c5d711b342 --- /dev/null +++ b/boards/shields/swir_hl78xx_ev_kit/Kconfig.shield @@ -0,0 +1,5 @@ +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +config SHIELD_SWIR_HL78XX_EV_KIT + def_bool $(shields_list_contains,swir_hl78xx_ev_kit) diff --git a/boards/shields/swir_hl78xx_ev_kit/doc/img/SW-Dev-RC76.3.webp b/boards/shields/swir_hl78xx_ev_kit/doc/img/SW-Dev-RC76.3.webp new file mode 100644 index 0000000000000000000000000000000000000000..f2f339ed522d643b14b71053f013ce45e54843f1 GIT binary patch literal 100678 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

l25ZCkQgGg0j7bGB*CRdvNB*dpe5&Nn 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%0D1#(MR;5USENpvqU!?Z{R2LkF+4rq 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 N%q`96jO}d!{|C$SjGzDj literal 0 HcmV?d00001 diff --git a/boards/shields/swir_hl78xx_ev_kit/doc/index.rst b/boards/shields/swir_hl78xx_ev_kit/doc/index.rst new file mode 100644 index 000000000000..cc6a9ce761f6 --- /dev/null +++ b/boards/shields/swir_hl78xx_ev_kit/doc/index.rst @@ -0,0 +1,79 @@ +.. _swir_hl78xx_ev_kit: + +HL/RC Module Evaluation Kit Shield +################################## + +Overview +******** + +Welcome to the HL78 module getting started guide. +This guide will help you set up the evaluation kit (eval kit) +for sending AT commands to the HL78 module and initiating data transmission. + +.. figure:: img/SW-Dev-RC76.3.webp + :align: center + :alt: HL/RC Module Evaluation Kit Shield Shield + + HL/RC Module Evaluation Kit Shield Shield (Credit: Sierrra Wireless) + +More information about the shield can be found at the `HL/RC Module Evaluation Kit Shield guide website`_. + +Pins Assignment of HL/RC Module Evaluation Kit Shield Shield +============================================================ ++--------------------------+----------------------------------------------------------+ +| Shield Connector Pin | Function | ++==========================+==========================================================+ +| CN403 alias | UART 1 (with CTS and RTS pins) | ++--------------------------+----------------------------------------------------------+ +| CN303 alias | SPI / UART 3 | ++--------------------------+----------------------------------------------------------+ +| CN1000 alias | GPIO Test Pins | ++--------------------------+----------------------------------------------------------+ +| GPIO6 CN1000_3 | LOW POWER MONITORING | ++--------------------------+----------------------------------------------------------+ +| VGPIO alias | Indirect indicator of hibernate mode entry/exit | ++--------------------------+----------------------------------------------------------+ +| RESET CN1000_12 | RESET SIGNAL | ++--------------------------+----------------------------------------------------------+ +| WAKE-UP CN1000_8 | SPI / UART 3 | ++--------------------------+----------------------------------------------------------+ + +Please refer to the website for more information about HL/RC Module Evaluation Kit Shield Shield setup. +.. _HL/RC Module Evaluation Kit Shield guide website: + +Checking Your Basic Configurations in PuTTY +=========================================== +Before trying to set up a wired connection between the board and a host MCU, +it's a good idea to first go through this list of basic AT commands over a +USB COM port on a PC. For reference, you can find all the AT commands for the +HL78xx modules in the Source. + +Requirements +************ + +This shield can be used with any boards which provides a configuration for +header connectors and defines node aliases for UART, SPI and USB interfaces (see +:ref:`shields` for more details). + +Programming +*********** + +Set ``--shield swir_hl78xx_ev_kit`` when you invoke ``west build``. For +example: + +.. zephyr-app-commands:: + :zephyr-app: samples/drivers/modem/hello_hl78xx + :board: st/nucleo_u575zi_q + :shield: swir_hl78xx_ev_kit + :goals: build + +References +********** + +.. target-notes:: + +.. _HL/RC Module Evaluation Kit Shield guide website: + https://source.sierrawireless.com/resources/airprime/development_kits/hl78xx-hl7900-development-kit-guide/ + +.. _HL/RC Module Evaluation Kit Shield specification website: + https://info.sierrawireless.com/iot-modules-evaluation-kit#guide-for-the-hl78-series-evaluation-kit diff --git a/boards/shields/swir_hl78xx_ev_kit/swir_hl78xx_ev_kit.overlay b/boards/shields/swir_hl78xx_ev_kit/swir_hl78xx_ev_kit.overlay new file mode 100644 index 000000000000..b7408f4cd3d8 --- /dev/null +++ b/boards/shields/swir_hl78xx_ev_kit/swir_hl78xx_ev_kit.overlay @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + aliases { + modem-uart = &usart2; + modem = &modem; + }; +}; + +&usart2 { + pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3 &usart2_rts_pd4 &usart2_cts_pd3 >; + pinctrl-1 = <&analog_pa2 &analog_pa3 &analog_pd4 &analog_pd3 >; + dmas = <&gpdma1 0 27 STM32_DMA_PERIPH_TX + &gpdma1 1 26 STM32_DMA_PERIPH_RX>; + dma-names = "tx", "rx"; + pinctrl-names = "default", "sleep"; + current-speed = <115200>; + status = "okay"; + hw-flow-control; + modem: hl_modem { + compatible = "swir,hl7812"; + status = "okay"; + mdm-reset-gpios = <&gpiod 5 (GPIO_ACTIVE_LOW)>; + mdm-wake-gpios = <&gpioe 15 (GPIO_ACTIVE_HIGH)>; + mdm-vgpio-gpios = <&gpiob 2 0>; + mdm-uart-cts-gpios = <&gpiod 3 0>; + mdm-gpio6-gpios = <&gpioa 8 0>; + }; + +}; diff --git a/dts/bindings/modem/swir,hl7812.yaml b/dts/bindings/modem/swir,hl7812.yaml new file mode 100644 index 000000000000..a75d0c044b40 --- /dev/null +++ b/dts/bindings/modem/swir,hl7812.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025, Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +description: Sierra Wireless HL7812 Modem + +compatible: "swir,hl7812" + +include: swir,hl78xx.yaml diff --git a/dts/bindings/modem/swir,hl78xx.yaml b/dts/bindings/modem/swir,hl78xx.yaml new file mode 100644 index 000000000000..ab9c7b98ed1b --- /dev/null +++ b/dts/bindings/modem/swir,hl78xx.yaml @@ -0,0 +1,38 @@ +# Copyright (c) 2025, Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +description: Sierra Wireless HL78XX Modem + +compatible: "swir,hl78xx" + +include: + - uart-device.yaml + +properties: + mdm-wake-gpios: + type: phandle-array + required: true + + mdm-reset-gpios: + type: phandle-array + required: true + + mdm-pwr-on-gpios: + type: phandle-array + + mdm-fast-shutd-gpios: + type: phandle-array + + mdm-vgpio-gpios: + type: phandle-array + required: true + + mdm-uart-dsr-gpios: + type: phandle-array + + mdm-uart-cts-gpios: + type: phandle-array + required: true + + mdm-gpio6-gpios: + type: phandle-array From e01854ed2c6d625657510dae097d784d9f92b7c8 Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Mon, 30 Jun 2025 22:05:31 +0100 Subject: [PATCH 6/7] drivers: modem: HL78XX Modem Driver Low Power Adding Low Power Functionalities Signed-off-by: Zafer SEN --- drivers/modem/hl78xx/Kconfig.hl78xx | 225 ++++- drivers/modem/hl78xx/hl78xx.c | 855 +++++++++++++++--- drivers/modem/hl78xx/hl78xx.h | 277 +++++- drivers/modem/hl78xx/hl78xx_apis.c | 2 +- .../Kconfig.hl78xx_evt_monitor | 2 +- drivers/modem/hl78xx/hl78xx_sockets.c | 493 ++++++---- drivers/modem/hl78xx/hl78xx_utility.c | 69 +- include/zephyr/drivers/modem/hl78xx_apis.h | 13 +- 8 files changed, 1657 insertions(+), 279 deletions(-) diff --git a/drivers/modem/hl78xx/Kconfig.hl78xx b/drivers/modem/hl78xx/Kconfig.hl78xx index 626640f09855..e9f5a8825993 100644 --- a/drivers/modem/hl78xx/Kconfig.hl78xx +++ b/drivers/modem/hl78xx/Kconfig.hl78xx @@ -1,7 +1,7 @@ -# Sierra Wireless HL78XX driver driver options +#Sierra Wireless HL78XX driver driver options -# Copyright (c) 2025 Netfeasa Ltd. -# SPDX-License-Identifier: Apache-2.0 +#Copyright(c) 2025 Netfeasa Ltd. +#SPDX - License - Identifier : Apache - 2.0 config MODEM_HL78XX bool "HL78XX modem driver" @@ -492,15 +492,36 @@ config MODEM_HL78XX_PSM help Enable Power Save Mode (PSM) +if !MODEM_HL78XX_EDRX && !MODEM_HL78XX_PSM + +config MODEM_HL78XX_POWER_DOWN + bool "Turn OFF" + depends on MODEM_HL78XX_LOW_POWER_MODE + help + Power off modem + +endif # If eDRX or PSM is enabled, the modem will not power down. + if MODEM_HL78XX_EDRX config MODEM_HL78XX_EDRX_VALUE - string "Requested eDRX timer" - default "0101" + int "Requested eDRX timer" + default 5 + range 0 15 help Half a byte in a 4-bit format. The eDRX value refers to bit 4 to 1 of octet 3 of the Extended DRX parameters information element. - Default value is 81.92 seconds. + Default value is 5 (81.92) seconds. + refer to Requested_eDRX_value + +config MODEM_HL78XX_PTW_VALUE + int "Requested eDRX Paging Time Window (PTW) timer" + default 0 + help + The PTW value refers to bits 8 to 5 of octet 3 of the Extended DRX + parameters information element + · CAT-M1 - actual PTW length = 1.28 sec x (1 + PTW) + · NB-IoT - actual PTW length = 2.56 sec x (1 + PTW) endif # MODEM_HL78XX_EDRX @@ -508,11 +529,11 @@ if MODEM_HL78XX_PSM config MODEM_HL78XX_PSM_PERIODIC_TAU string "Requested extended periodic TAU timer" - default "10000010" + default "10101111" help Requested extended periodic TAU (tracking area update) value (T3412) to be allocated to the UE in E-UTRAN. One byte in an 8-bit format. - Default value is 1 minute. + Default value is 15 minute. config MODEM_HL78XX_PSM_ACTIVE_TIME string "Requested active time" @@ -523,6 +544,62 @@ config MODEM_HL78XX_PSM_ACTIVE_TIME endif # MODEM_HL78XX_PSM +if MODEM_HL78XX_POWER_DOWN + +choice MODEM_HL78XX_POWER_DOWN_MODE + prompt "Power Down Mode" + default MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN + help + Choose the logic used to determine when the HL78XX modem powers down. + Only one method can be selected: + - 'Use delay after last data activity' + - 'Use active time after RRC connection' + +config MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN + bool "Use delay after last data activity" + help + Power down the modem after a specified delay (MODEM_HL78XX_POWER_DOWN_DELAY) + following the last socket closure or data transmission. + +config MODEM_HL78XX_USE_ACTIVE_TIME_BASED_POWER_DOWN + bool "Use active time after RRC connection" + help + Power down the modem based on a PSM-like active time (MODEM_HL78XX_POWER_DOWN_ACTIVE_TIME) + starting after establishing an RRC connection. + * This timer is reset only when a new RRC connection is established. + +endchoice + +if MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN + +config MODEM_HL78XX_POWER_DOWN_DELAY + int "Delay in seconds before power down" + default 15 + range 10 600 + help + Delay in seconds before powering down the modem after the last + data transfer. + +endif # MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN + +if MODEM_HL78XX_USE_ACTIVE_TIME_BASED_POWER_DOWN + +config MODEM_HL78XX_POWER_DOWN_ACTIVE_TIME + int "Modem active time before power down" + default 30 + range 20 600 + help + The active time value to be allocated to the UE before + powering down the modem. One byte in an 8-bit format. + Default value is 30 seconds. + This setting is used to keep the modem active for a short period + before powering it down. + +endif # MODEM_HL78XX_USE_ACTIVE_TIME_BASED_POWER_DOWN +endif # MODEM_HL78XX_POWER_DOWN + +if !MODEM_HL78XX_POWER_DOWN + choice MODEM_DEFAULT_SLEEP_LEVEL prompt "Default Sleep Level" default MODEM_HL78XX_SLEEP_LEVEL_HIBERNATE @@ -555,9 +632,111 @@ endchoice config MODEM_HL78XX_SLEEP_DELAY_AFTER_REBOOT int "Delay in seconds before sleep after reboot" default 10 - +endif # !MODEM_HL78XX_POWER_DOWN endif # MODEM_HL78XX_LOW_POWER_MODE +choice MODEM_HL78XX_NETWORK_REG_STATUS_REPORT_CFG + prompt "Network Registration Status Report Configuration" + default MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_PSM_AND_CAUSE + help + · 0 — Disable network registration unsolicited result code. + · 1 — Enable network registration unsolicited result code +CEREG: + · 2 — Enable network registration and location information unsolicited result + code: + +CEREG: [,[],[],[]] + · 3 — Enable network registration, location information and EMM cause value + information unsolicited result code: + +CEREG: [,[],[],[][,, ]] + · 4 — For a UE that wants to apply PSM, enable network registration and + location information unsolicited result code: + +CEREG: [,[],[],[][,,[,[],[]]]] + · 5 — For a UE that wants to apply PSM, enable network registration, location + information and EMM cause value information unsolicited result code: + +CEREG: [,[],[],[][,[],[][,[] []]]] + +config MODEM_HL78XX_DISABLE_NETWORK_STATUS_URC_REPORT + bool "Disable network status URC report" + help + Disable network registration unsolicited result code. + +config MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT + bool "Network status URC report" + help + Enable network registration unsolicited result code +CEREG: + +config MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_LOCATION + bool "Network status URC report with location" + help + Enable network registration and location information unsolicited result + +CEREG: [,[],[],[]] + +config MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_LOCATION_AND_CAUSE + bool "Network status URC report with location and cause" + help + Enable network registration, location information and EMM cause value + information unsolicited result code: + +CEREG: [,[],[],[][,, ]] + +config MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_PSM + bool "Network status URC report with PSM" + help + For a UE that wants to apply PSM, enable network registration and + location information unsolicited result code: + +CEREG: [,[],[],[][,,[,[],[]]]] + +config MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_PSM_AND_CAUSE + bool "Network status URC report with PSM and cause" + help + For a UE that wants to apply PSM, enable network registration, location + information and EMM cause value information unsolicited result code: + +CEREG: [,[],[],[][,[],[][,[] []]]] + +endchoice + +config MODEM_HL78XX_NETWORK_REG_STATUS_REPORT_CFG_CODE + string + default "5" if MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_PSM_AND_CAUSE + default "4" if MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_PSM + default "3" if MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_LOCATION_AND_CAUSE + default "2" if MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT_WITH_LOCATION + default "1" if MODEM_HL78XX_ENABLE_NETWORK_STATUS_URC_REPORT + default "0" if MODEM_HL78XX_DISABLE_NETWORK_STATUS_URC_REPORT + help + This setting is used to configure the network registration status report + configuration code. It is used in the AT+CREG/CEREG command to set the network + registration status report configuration. + +config MODEM_MIN_ALLOWED_SIGNAL_STRENGTH + int "Minimum allowed RSRP signal strength (dBm)" + default -140 + range -140 0 + help + The average power received from a single Reference signal, + and Its typical range is around -44dbm (good) to -140dbm(bad). + Note: (Anything < - 115 dBm unusable/unreliable) + EXCELLENT_SIGNAL_STRENGTH + bool ">= -80(dBm)" + default -80 + range - 80 0 + GOOD_SIGNAL_STRENGTH + bool ">= -90(dBm)" + default -90 + range - 90 0 + MID_CELL_SIGNAL_STRENGTH + bool ">= -100(dBm)" + default -100 + range - 100 0 + CELL_EDGE_SIGNAL_STRENGTH + bool "<= -100(dBm)" + default -110 + range - 110 0 + POOR_SIGNAL_STRENGTH + bool ">= -140(dBm)" + default -140 + range - 140 0 + config MODEM_HL78XX_ADVANCED_SOCKET_CONFIG bool "Advanced socket configuration" help @@ -603,6 +782,34 @@ bool "Verbose debug output in the HL78xx" Enabling this setting will turn on VERY heavy debugging from the modem. Do NOT leave on for production. +config MODEM_HL78XX_DEV_POWER_PULSE_DURATION + int "Duration of the power-on pulse in milliseconds." + default 1500 + help + Trigger a power-on sequence by setting a power on GPIO pin high + for a specific amount of time. + +config MODEM_HL78XX_DEV_RESET_PULSE_DURATION + int "Duration of the power-reset pulse in milliseconds." + default 100 + help + Trigger a power-reset sequence by setting a reset GPIO pin high + for a specific amount of time. + +config MODEM_HL78XX_DEV_STARTUP_TIME + int "Wait before assuming the device is ready." + default 1000 + help + The expected time (in milliseconds) the modem needs to fully power on + and become operational. + +config MODEM_HL78XX_DEV_SHUTDOWN_TIME + int "Wait before assuming the device is completely off." + default 1000 + help + The amount of time (in milliseconds) the system should wait for the modem + to fully shut down + config MODEM_HL78XX_DEV_INIT_PRIORITY int "Sierra Wireless HL78XX device driver init priority" default 80 diff --git a/drivers/modem/hl78xx/hl78xx.c b/drivers/modem/hl78xx/hl78xx.c index 7862b93d8b0c..1b396cd79aa1 100644 --- a/drivers/modem/hl78xx/hl78xx.c +++ b/drivers/modem/hl78xx/hl78xx.c @@ -48,8 +48,11 @@ hl78xx_evt_monitor_handler_t event_dispatcher; static void hl78xx_event_handler(struct hl78xx_data *data, enum hl78xx_event evt); static int hl78xx_on_idle_state_enter(struct hl78xx_data *data); -static void hl78xx_begin_power_off_pulse(struct hl78xx_data *data); - +static int hl78xx_on_state_enter(struct hl78xx_data *data); +static int hl78xx_on_state_leave(struct hl78xx_data *data); +#ifdef CONFIG_MODEM_HL78XX_POWER_DOWN +static int hl78xx_pwr_dwn_feed_timer(struct hl78xx_data *data); +#endif static void event_dispatcher_dispatch(struct hl78xx_evt *notif) { if (event_dispatcher != NULL) { @@ -74,12 +77,16 @@ static const char *hl78xx_state_str(enum hl78xx_state state) return "run init script"; case MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT: return "init fail diagnostic script "; - case MODEM_HL78XX_STATE_AWAIT_REGISTERED: - return "await registered"; + case MODEM_HL78XX_STATE_RUN_PMC_CONFIG_SCRIPT: + return "run pmc cfg script"; case MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT: return "run rat cfg script"; case MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT: return "run enable gprs script"; + case MODEM_HL78XX_STATE_AWAIT_WAKEUP: + return "await wakeup"; + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: + return "await registered"; case MODEM_HL78XX_STATE_CARRIER_ON: return "carrier on"; case MODEM_HL78XX_STATE_CARRIER_OFF: @@ -126,6 +133,12 @@ static const char *hl78xx_event_str(enum hl78xx_event event) return "bus closed"; case MODEM_HL78XX_EVENT_SOCKET_READY: return "socket ready"; + case MODEM_HL78XX_EVENT_SOCKET_CLOSED: + return "socket closed"; + case MODEM_HL78XX_EVENT_DEVICE_AWAKE: + return "device awake"; + case MODEM_HL78XX_EVENT_DEVICE_ASLEEP: + return "device asleep"; default: return "unknown event"; } @@ -230,26 +243,49 @@ static void hl78xx_on_cxreg(struct modem_chat *chat, char **argv, uint16_t argc, struct hl78xx_data *data = (struct hl78xx_data *)user_data; enum hl78xx_registration_status registration_status = 0; - if (argc >= 2) { - /* +CEREG: [,[...]] */ - registration_status = atoi(argv[1]); - } else { + if (argc < 2) { return; } #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d %s:%d", __LINE__, __func__, argc, argv[0], registration_status); + LOG_DBG("%d %s %d %s", __LINE__, __func__, argc, argv[0]); #endif - data->status.registration.network_state = registration_status; + if (argc > 2 && strlen(argv[1]) == 1 && strlen(argv[2]) == 1) { + /* This is a condition to distinguish received message between URC message and User + * request network status request. If the message is User message, then argv[1] and + * argv[2] will be 1 character long. If the message is URC request network status + * request, then argv[1] will be 1 character long while argv[2] will be 2 characters + * long. + */ + registration_status = atoi(argv[2]); + } else { + registration_status = atoi(argv[1]); + } + + if (registration_status == data->status.registration.network_state_current) { + LOG_DBG("Registration status unchanged: %d", registration_status); + return; + } + + data->status.registration.network_state_previous = + data->status.registration.network_state_current; + /* Update the current registration state */ + data->status.registration.network_state_current = registration_status; struct hl78xx_evt event = {.type = HL78XX_LTE_REGISTRATION_STAT_UPDATE, - .content.reg_status = data->status.registration.network_state}; + .content.reg_status = + data->status.registration.network_state_current}; event_dispatcher_dispatch(&event); - + data->status.registration.is_registered_previously = + data->status.registration.is_registered_currently; if (hl78xx_is_registered(data)) { - data->status.registration.is_registered = true; + data->status.registration.is_registered_currently = true; hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_REGISTERED); +#if defined CONFIG_MODEM_HL78XX_POWER_DOWN && \ + defined CONFIG_MODEM_HL78XX_USE_ACTIVE_TIME_BASED_POWER_DOWN + hl78xx_pwr_dwn_feed_timer(data); +#endif } else { - data->status.registration.is_registered = false; + data->status.registration.is_registered_currently = false; hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_DEREGISTERED); } } @@ -257,25 +293,36 @@ static void hl78xx_on_cxreg(struct modem_chat *chat, char **argv, uint16_t argc, static void hl78xx_on_psmev(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) { #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d", __LINE__, __func__, argc); - if (argc >= 2) { - /* +CEREG: [,[...]] */ - LOG_DBG("%d %s %s", __LINE__, __func__, argv[1]); - } else { - LOG_DBG("%d %s %s", __LINE__, __func__, argv[0]); - } #endif + if (argc < 2) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + data->status.psm.psmev_previous = data->status.psm.psmev_current; + data->status.psm.psmev_current = ATOI(argv[1], 0, "psmev"); + struct hl78xx_evt event = {.type = HL78XX_LTE_PSMEV, + .content.psm_event = data->status.psm.psmev_current}; + + event_dispatcher_dispatch(&event); } static void hl78xx_on_ksup(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) { - char ksup_data[2] = {0}; + if (argc != 2) { + return; + } + + int module_status = ATOI(argv[1], 0, "ksup"); - strncpy(ksup_data, argv[1], sizeof(ksup_data) - 1); #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("Module status: %s", ksup_data); + LOG_DBG("Module status: %d", module_status); #endif + struct hl78xx_evt event = {.type = HL78XX_LTE_MODEM_STARTUP, + .content.value = module_status}; + + event_dispatcher_dispatch(&event); } static void hl78xx_on_imei(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) @@ -407,12 +454,34 @@ static void hl78xx_on_socknotifydata(struct modem_chat *chat, char **argv, uint1 if (argc < 2) { return; } - socket_id = ATOI(argv[1], 0, "socket_id"); - new_total = ATOI(argv[2], 0, "length"); + socket_id = ATOI(argv[1], -1, "socket_id"); + new_total = ATOI(argv[2], -1, "length"); #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d %d", __LINE__, __func__, socket_id, new_total); + LOG_DBG("%d %d %d", __LINE__, socket_id, new_total); #endif - socknotifydata(socket_id, new_total); + socket_notify_data(socket_id, new_total); +} + +static void hl78xx_on_ktcpnotif(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + int socket_id = -1; + int tcp_notif = -1; + + if (argc < 2) { + return; + } + socket_id = ATOI(argv[1], -1, "socket_id"); + tcp_notif = ATOI(argv[2], -1, "tcp_notif"); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d %d", __LINE__, socket_id, tcp_notif); +#endif + + if (tcp_notif == -1) { + return; + } + + tcp_notify_data(socket_id, tcp_notif); } static void hl78xx_on_ksrep(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) @@ -448,8 +517,7 @@ static void hl78xx_on_ksrat(struct modem_chat *chat, char **argv, uint16_t argc, LOG_DBG("KSRAT: %s %s", argv[0], argv[1]); #endif } -#endif /* MODEM_HL78XX_RAT */ - +#else static void hl78xx_on_kselacq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) { if (argc < 2) { @@ -474,6 +542,7 @@ static void hl78xx_on_kselacq(struct modem_chat *chat, char **argv, uint16_t arg data->kselacq_data.rat1, data->kselacq_data.rat2, data->kselacq_data.rat3); #endif } +#endif /* CONFIG_MODEM_HL78XX_AUTORAT */ static void hl78xx_on_kbndcfg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) { @@ -498,6 +567,7 @@ static void hl78xx_on_kbndcfg(struct modem_chat *chat, char **argv, uint16_t arg if (rat_id >= HL78XX_RAT_COUNT) { return; } + data->status.kbndcfg[rat_id].rat = rat_id; strncpy(data->status.kbndcfg[rat_id].bnd_bitmap, argv[2], kbnd_bitmap_size); data->status.kbndcfg[rat_id].bnd_bitmap[kbnd_bitmap_size] = '\0'; @@ -528,8 +598,28 @@ static void hl78xx_on_cesq(struct modem_chat *chat, char **argv, uint16_t argc, LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); #endif - data->status.rsrq = ATOI(argv[5], 0, "rssi"); - data->status.rsrp = ATOI(argv[6], 0, "rssi"); + data->status.rsrq = ATOI(argv[5], 0, "rssq"); + data->status.rsrp = ATOI(argv[6], 0, "rssp"); +} + +static void hl78xx_on_kcellmeas(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + if (argc < 5) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); +#endif + data->status.rsrp = (int)ATOD(argv[1], 0, "rsrp"); + LOG_DBG("%d %s RSRP: %d", __LINE__, __func__, data->status.rsrp); + if (hl78xx_is_rsrp_valid(data)) { + data->status.registration.is_registered_currently = true; + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_REGISTERED); + hl78xx_release_socket_comms(); + } } static void hl78xx_on_cfun(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) @@ -546,6 +636,84 @@ static void hl78xx_on_cfun(struct modem_chat *chat, char **argv, uint16_t argc, data->status.phone_functionality = ATOI(argv[1], 0, "phone_func"); } +static void hl78xx_on_ksleep(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] ", __LINE__, argc, argv[0], argv[1]); +#endif + if (argc > 3) { + data->status.pmc_sleep.mngt = ATOI(argv[1], 0, "mngt"); + data->status.pmc_sleep.level = ATOI(argv[2], 0, "level"); + data->status.pmc_sleep.delay = ATOI(argv[3], 0, "delay"); + } else { + /* only the case when the modem sleep mode is always disabled + * AT+KSLEEP? + * +KSLEEP: 2 + * OK + */ + data->status.pmc_sleep.mngt = ATOI(argv[1], 0, "mngt"); + data->status.pmc_sleep.level = 0; + data->status.pmc_sleep.delay = 0; + } +} +static void hl78xx_on_cpsms(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[4], argv[5]); +#endif + if (argc > 3) { + data->status.pmc_cpsms.mode = ATOI(argv[1], 0, "mode"); + int8_t active_time = binary_str_to_byte(argv[5]); + int8_t periodic_tau = binary_str_to_byte(argv[4]); + + data->status.pmc_cpsms.active_time = (active_time == -EINVAL) ? 0 : active_time; + data->status.pmc_cpsms.periodic_tau = (periodic_tau == -EINVAL) ? 0 : periodic_tau; + } else { + data->status.pmc_cpsms.mode = ATOI(argv[1], 0, "mode"); + data->status.pmc_cpsms.active_time = 0; + data->status.pmc_cpsms.periodic_tau = 0; + } +} +static void hl78xx_on_kedrxcfg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 3) { + return; + } + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + int act_type = ATOI(argv[2], -1, "act_type"); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d [%s] [%s] ", __LINE__, argc, argv[0], argv[1]); +#endif + if (act_type < 3 || act_type > 5) { + LOG_ERR("Invalid act_type %d", act_type); + return; + } + + if (argc > 3) { + data->status.pmc_kedrxcfg[act_type - HL78XX_ACT_TYPE_RAT_MASK].mode = + ATOI(argv[1], 0, "mode"); + data->status.pmc_kedrxcfg[act_type - HL78XX_ACT_TYPE_RAT_MASK].ack_type = + ATOI(argv[2], 0, "ack_type"); + data->status.pmc_kedrxcfg[act_type - HL78XX_ACT_TYPE_RAT_MASK].requested_edrx = + ATOI(argv[3], 0, "r_edrx"); + } else { + data->status.pmc_kedrxcfg[act_type - HL78XX_ACT_TYPE_RAT_MASK].mode = + ATOI(argv[1], 0, "mode"); + data->status.pmc_kedrxcfg[act_type - HL78XX_ACT_TYPE_RAT_MASK].ack_type = + ATOI(argv[2], 0, "ack_type"); + data->status.pmc_kedrxcfg[act_type - HL78XX_ACT_TYPE_RAT_MASK].requested_edrx = 0; + } +} MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); MODEM_CHAT_MATCHES_DEFINE(allow_match, MODEM_CHAT_MATCH("OK", "", NULL), MODEM_CHAT_MATCH("+CME ERROR: ", "", NULL)); @@ -557,11 +725,14 @@ MODEM_CHAT_MATCHES_DEFINE(unsol_matches, MODEM_CHAT_MATCH("+CREG: ", ",", hl78xx MODEM_CHAT_MATCH("+KSTATEV: ", ",", hl78xx_on_kstatev), MODEM_CHAT_MATCH("+KUDP_DATA: ", ",", hl78xx_on_socknotifydata), MODEM_CHAT_MATCH("+KTCP_DATA: ", ",", hl78xx_on_socknotifydata), + MODEM_CHAT_MATCH("+KTCP_NOTIF: ", ",", hl78xx_on_ktcpnotif), MODEM_CHAT_MATCH("+KUDP_RCV: ", ",", hl78xx_on_udprcv), MODEM_CHAT_MATCH("+KBNDCFG: ", ",", hl78xx_on_kbndcfg), MODEM_CHAT_MATCH("+CSQ: ", ",", hl78xx_on_csq), MODEM_CHAT_MATCH("+CESQ: ", ",", hl78xx_on_cesq), - MODEM_CHAT_MATCH("+CFUN: ", "", hl78xx_on_cfun)); + MODEM_CHAT_MATCH("+KCELLMEAS: ", ",", hl78xx_on_kcellmeas), + MODEM_CHAT_MATCH("+CFUN: ", "", hl78xx_on_cfun), + MODEM_CHAT_MATCH("+KEDRXCFG: ", ",", hl78xx_on_kedrxcfg)); MODEM_CHAT_MATCHES_DEFINE(abort_matches, MODEM_CHAT_MATCH("ERROR", "", NULL)); MODEM_CHAT_MATCH_DEFINE(at_ready_match, "+KSUP: ", "", hl78xx_on_ksup); @@ -574,8 +745,11 @@ MODEM_CHAT_MATCH_DEFINE(iccid_match, "+CCID: ", "", hl78xx_on_iccid); MODEM_CHAT_MATCH_DEFINE(ksrep_match, "+KSREP: ", ",", hl78xx_on_ksrep); #ifndef CONFIG_MODEM_HL78XX_AUTORAT MODEM_CHAT_MATCH_DEFINE(ksrat_match, "+KSRAT: ", "", hl78xx_on_ksrat); -#endif /* CONFIG_MODEM_HL78XX_RAT */ +#else MODEM_CHAT_MATCH_DEFINE(kselacq_match, "+KSELACQ: ", ",", hl78xx_on_kselacq); +#endif /* CONFIG_MODEM_HL78XX_RAT */ +MODEM_CHAT_MATCH_DEFINE(ksleep_match, "+KSLEEP: ", ",", hl78xx_on_ksleep); +MODEM_CHAT_MATCH_DEFINE(cpsms_match, "+CPSMS: ", ",", hl78xx_on_cpsms); static void hl78xx_init_pipe(const struct device *dev) { @@ -613,38 +787,55 @@ static int modem_init_chat(const struct device *dev) return modem_chat_init(&data->chat, &chat_config); } +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_periodic_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSIMDET?", ok_match), + /* MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG?", ok_match) */); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_periodic_chat_script, hl78xx_periodic_chat_script_cmds, + abort_matches, hl78xx_chat_callback_handler, 4); MODEM_CHAT_SCRIPT_CMDS_DEFINE( - swir_hl78xx_init_chat_script_cmds, MODEM_CHAT_SCRIPT_CMD_RESP("", at_ready_match), + hl78xx_init_chat_script_cmds, MODEM_CHAT_SCRIPT_CMD_RESP("", at_ready_match), +#ifdef CONFIG_MODEM_HL78XX_LOW_POWER_MODE MODEM_CHAT_SCRIPT_CMD_RESP("AT+KHWIOCFG=3,1,6", ok_match), +#endif MODEM_CHAT_SCRIPT_CMD_RESP("ATE0", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP_MULT("AT+CGACT=0", allow_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+CFUN=4", ok_match), - MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSLEEP=2", ok_match), - MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPSMS=0", ok_match), - MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEDRXS=0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSLEEP?", ksleep_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPSMS?", cpsms_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KEDRXCFG?", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+KPATTERN=" "\"" EOF_PATTERN "\"", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+CCID", iccid_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+CMEE=1", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+GNSSCONF=10,1", ok_match), - MODEM_CHAT_SCRIPT_CMD_RESP("AT+GNSSNMEA=0,1000,0,4F", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+GNSSNMEA=0,1000,0,104F", 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_CMD_RESP("AT+CIMI", cimi_match), MODEM_CHAT_SCRIPT_CMD_RESP("", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSTATEV=1", ok_match), +#ifdef CONFIG_MODEM_HL78XX_LOW_POWER_MODE + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KPSMEV=1", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KCELLMEAS=1,35", ok_match), +#endif MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGEREP=2", ok_match), +#ifdef CONFIG_MODEM_HL78XX_AUTORAT MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSELACQ?", kselacq_match), +#else + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSRAT?", ksrat_match), +#endif MODEM_CHAT_SCRIPT_CMD_RESP("AT+KBNDCFG?", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGACT?", ok_match), MODEM_CHAT_SCRIPT_CMD_RESP("AT+CREG=0", ok_match), - MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG=5", ok_match)); + MODEM_CHAT_SCRIPT_CMD_RESP( + "AT+CEREG=" CONFIG_MODEM_HL78XX_NETWORK_REG_STATUS_REPORT_CFG_CODE, ok_match)); -MODEM_CHAT_SCRIPT_DEFINE(swir_hl78xx_init_chat_script, swir_hl78xx_init_chat_script_cmds, - abort_matches, hl78xx_chat_callback_handler, 10); +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_init_chat_script, hl78xx_init_chat_script_cmds, abort_matches, + hl78xx_chat_callback_handler, 100); int modem_cmd_send_int(struct hl78xx_data *data, modem_chat_script_callback script_user_callback, const uint8_t *cmd, uint16_t cmd_size, @@ -652,7 +843,11 @@ int modem_cmd_send_int(struct hl78xx_data *data, modem_chat_script_callback scri bool user_cmd) { int ret = 0; - +#if defined CONFIG_MODEM_HL78XX_POWER_DOWN && defined CONFIG_MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN + if (data->status.state == MODEM_HL78XX_STATE_CARRIER_ON) { + hl78xx_pwr_dwn_feed_timer(data); + } +#endif ret = k_mutex_lock(&data->tx_lock, K_NO_WAIT); if (ret < 0) { if (user_cmd == false) { @@ -694,48 +889,95 @@ int modem_cmd_send_int(struct hl78xx_data *data, modem_chat_script_callback scri void mdm_vgpio_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) { - ARG_UNUSED(port); - ARG_UNUSED(cb); - ARG_UNUSED(pins); + struct hl78xx_data *data = CONTAINER_OF(cb, struct hl78xx_data, gpio_cbs.vgpio_cb); + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + const struct gpio_dt_spec *spec = &config->mdm_gpio_vgpio; + + if (spec == NULL || spec->port == NULL) { + LOG_ERR("VGPIO GPIO spec is not configured properly"); + return; + } - const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + if (!(pins & BIT(spec->pin))) { + return; /* not our pin */ + } - LOG_DBG("VGPIO ISR callback %d", gpio_pin_get_dt(&spec)); + LOG_DBG("VGPIO ISR callback %s %d %d", spec->port->name, spec->pin, gpio_pin_get_dt(spec)); } -#if DT_INST_NODE_HAS_PROP(0, mdm_uart_dsr_gpios) +#if HAS_UART_DSR_GPIO void mdm_uart_dsr_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) { - ARG_UNUSED(port); - ARG_UNUSED(cb); - ARG_UNUSED(pins); + struct hl78xx_data *data = CONTAINER_OF(cb, struct hl78xx_data, gpio_cbs.vgpio_cb); + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + const struct gpio_dt_spec *spec = &config->mdm_gpio_uart_dsr; - const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + if (spec == NULL || spec->port == NULL) { + LOG_ERR("DSR GPIO spec is not configured properly"); + return; + } + if (!(pins & BIT(spec->pin))) { + return; /* not our pin */ + } - LOG_DBG("DSR ISR callback %d", gpio_pin_get_dt(&spec)); + LOG_DBG("DSR ISR callback %d", gpio_pin_get_dt(spec)); } #endif void mdm_gpio6_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) { - ARG_UNUSED(port); - ARG_UNUSED(cb); - ARG_UNUSED(pins); + struct hl78xx_data *data = CONTAINER_OF(cb, struct hl78xx_data, gpio_cbs.gpio6_cb); + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + const struct gpio_dt_spec *spec = &config->mdm_gpio_gpio6; + enum pm_device_state state; + int rc = 0; + + if (spec == NULL || spec->port == NULL) { + LOG_ERR("GPIO6 GPIO spec is not configured properly"); + return; + } + + if (!(pins & BIT(spec->pin))) { + return; /* not our pin */ + } - const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + bool pin_state = gpio_pin_get_dt(spec); - LOG_DBG("GPIO6 ISR callback %d", gpio_pin_get_dt(&spec)); + LOG_DBG("GPIO6 ISR callback %s %d %d", spec->port->name, spec->pin, pin_state); + + rc = pm_device_state_get(config->uart, &state); + if (rc == 0) { + LOG_DBG("PM state %d ret: %d", state, rc); + } + + if (!pin_state) { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_DEVICE_ASLEEP); + data->status.pmc_power_down.status_previously = + data->status.pmc_power_down.status_currently; + data->status.pmc_power_down.status_currently = true; + } else { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_DEVICE_AWAKE); + data->status.pmc_power_down.status_previously = + data->status.pmc_power_down.status_currently; + data->status.pmc_power_down.status_currently = false; + } } void mdm_uart_cts_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t pins) { - ARG_UNUSED(port); - ARG_UNUSED(cb); - ARG_UNUSED(pins); + struct hl78xx_data *data = CONTAINER_OF(cb, struct hl78xx_data, gpio_cbs.gpio6_cb); + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + const struct gpio_dt_spec *spec = &config->mdm_gpio_uart_cts; - const struct gpio_dt_spec spec = {.port = port, .pin = pins}; + if (spec == NULL || spec->port == NULL) { + LOG_ERR("CTS GPIO spec is not configured properly"); + return; + } + if (!(pins & BIT(spec->pin))) { + return; /* not our pin */ + } - LOG_DBG("CTS ISR callback %d", gpio_pin_get_dt(&spec)); + LOG_DBG("CTS ISR callback %d", gpio_pin_get_dt(spec)); } static int hl78xx_on_reset_pulse_state_enter(struct hl78xx_data *data) @@ -860,7 +1102,7 @@ static void hl78xx_run_init_script_event_handler(struct hl78xx_data *data, enum break; case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: - hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT); + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_PMC_CONFIG_SCRIPT); break; @@ -1024,6 +1266,7 @@ static int hl78xx_rat_cfg(struct hl78xx_data *data, bool *modem_require_restart, *rat_request = HL78XX_RAT_GSM; } + #ifdef CONFIG_MODEM_FW_R6 else if (IS_ENABLED(CONFIG_MODEM_HL78XX_RAT_NBNTN)) { cmd_set_rat = (const char *)SET_RAT_NBNTN_CMD_LEGACY; @@ -1031,6 +1274,7 @@ static int hl78xx_rat_cfg(struct hl78xx_data *data, bool *modem_require_restart, *rat_request = HL78XX_RAT_NBNTN; } #endif /* CONFIG_MODEM_FW_R6 */ + #endif else { LOG_ERR("%d %s No rat has been selected.", __LINE__, __func__); @@ -1052,6 +1296,7 @@ static int hl78xx_rat_cfg(struct hl78xx_data *data, bool *modem_require_restart, } #endif /* CONFIG_MODEM_HL78XX_AUTORAT */ + error: return ret; } @@ -1184,6 +1429,211 @@ static int hl78xx_on_run_rat_cfg_script_state_leave(struct hl78xx_data *data) { return 0; } +#ifndef CONFIG_MODEM_HL78XX_LOW_POWER_MODE +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_disable_pmc_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSLEEP=2", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPSMS=0", ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KEDRXCFG=0", ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_disable_pmc_chat_script, hl78xx_disable_pmc_chat_script_cmds, + abort_matches, hl78xx_chat_callback_handler, 10); + +static int hl78xx_disabe_pmc(struct hl78xx_data *data) +{ + LOG_DBG("%d Disabling Power Management Config", __LINE__); + return modem_chat_run_script_async(&data->chat, &hl78xx_disable_pmc_chat_script); +} +#else +static int hl78xx_enable_pmc(struct hl78xx_data *data) +{ + const char *turn_on_pmc_cmd = "AT+KSLEEP=1,2,0"; + + LOG_DBG("%d Enabling Power Management Config", __LINE__); + return modem_cmd_send_int(data, NULL, turn_on_pmc_cmd, strlen(turn_on_pmc_cmd), &ok_match, + 1, false); +} + +static int hl78xx_psm_settings(struct hl78xx_data *data) +{ + if (data->status.registration.rat_mode != HL78XX_RAT_NB1 && + data->status.registration.rat_mode != HL78XX_RAT_CAT_M1) { + LOG_DBG("PSM is not supported for RAT mode: %d", + data->status.registration.rat_mode); + return 0; + } +#ifdef CONFIG_MODEM_HL78XX_PSM + if (data->status.pmc_cpsms.mode == false) { + const char *turn_on_psm_cmd = "AT+CPSMS=1,,,\"" CONFIG_MODEM_HL78XX_PSM_PERIODIC_TAU + "\",\"" CONFIG_MODEM_HL78XX_PSM_ACTIVE_TIME "\""; + + return modem_cmd_send_int(data, NULL, turn_on_psm_cmd, strlen(turn_on_psm_cmd), + &ok_match, 1, false); + } +#else + if (data->status.pmc_cpsms.mode == true) { + const char *turn_off_psm_cmd = "AT+CPSMS=0"; + + return modem_cmd_send_int(data, NULL, turn_off_psm_cmd, strlen(turn_off_psm_cmd), + &ok_match, 1, false); + } +#endif + LOG_DBG("PSM is already configured for RAT mode: %d", data->status.registration.rat_mode); + return 0; /* PSM already disabled */ +} + +static int hl78xx_edrx_settings(struct hl78xx_data *data) +{ + if (data->status.registration.rat_mode != HL78XX_RAT_NB1 && + data->status.registration.rat_mode != HL78XX_RAT_CAT_M1) { + LOG_DBG("eDRX is not supported for RAT mode: %d", + data->status.registration.rat_mode); + return 0; + } +#ifdef CONFIG_MODEM_HL78XX_EDRX + if (data->status.pmc_kedrxcfg[data->status.registration.rat_mode].mode == + HL78XX_KEDRX_MODE_DISABLE || + data->status.pmc_kedrxcfg[data->status.registration.rat_mode].mode == + HL78XX_KEDRX_MODE_DISABLE_AND_ERASE_CFG) { + char turn_on_edrx_cmd[sizeof("AT+KEDRXCFG=1,X,XXXX,XXXX")] = {0}; + uint8_t ack_type = 4; + + ack_type = (data->status.registration.rat_mode == HL78XX_RAT_NB1) ? 5 : ack_type; + + snprintf(turn_on_edrx_cmd, sizeof(turn_on_edrx_cmd), "AT+KEDRXCFG=1,%hhu,%d,%d", + ack_type, CONFIG_MODEM_HL78XX_EDRX_VALUE, CONFIG_MODEM_HL78XX_PTW_VALUE); + + return modem_cmd_send_int(data, NULL, turn_on_edrx_cmd, strlen(turn_on_edrx_cmd), + &ok_match, 1, false); + } +#else + if (data->status.pmc_kedrxcfg[data->status.registration.rat_mode].mode == + HL78XX_KEDRX_MODE_ENABLE || + data->status.pmc_kedrxcfg[data->status.registration.rat_mode].mode == + HL78XX_KEDRX_MODE_ENABLE_W_URC) { + char *turn_off_edrx_cmd = "AT+KEDRXCFG=0"; + + return modem_cmd_send_int(data, NULL, turn_off_edrx_cmd, strlen(turn_off_edrx_cmd), + &ok_match, 1, false); + } +#endif + + LOG_DBG("eDRX is already configured for RAT mode: %d", data->status.registration.rat_mode); + return 0; +} +#ifdef CONFIG_MODEM_HL78XX_POWER_DOWN +void hl78xx_pwr_dwn_work_handler(struct k_work *work_item) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work_item); + struct hl78xx_data *data = CONTAINER_OF(dwork, struct hl78xx_data, hl78xx_pwr_dwn_work); + + LOG_DBG("%d %s: Power down work handler called", __LINE__, __func__); + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + data->status.pmc_power_down.requested_previously = + data->status.pmc_power_down.requested_currently; + data->status.pmc_power_down.requested_currently = true; +} + +static int hl78xx_pwr_dwn_feed_timer(struct hl78xx_data *data) +{ +#ifdef CONFIG_MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN + k_work_reschedule(&data->hl78xx_pwr_dwn_work, + K_SECONDS(CONFIG_MODEM_HL78XX_POWER_DOWN_DELAY)); +#else + k_work_reschedule(&data->hl78xx_pwr_dwn_work, + K_SECONDS(CONFIG_MODEM_HL78XX_POWER_DOWN_ACTIVE_TIME)); +#endif + data->status.pmc_power_down.requested_previously = + data->status.pmc_power_down.requested_currently; + data->status.pmc_power_down.requested_currently = false; + + return 0; +} + +static int hl78xx_pwr_dwn_settings(struct hl78xx_data *data) +{ + LOG_DBG("%d Modem Power Down Settings", __LINE__); + return 0; +} +#endif +#endif /* CONFIG_MODEM_HL78XX_LOW_POWER_MODE */ + +static int hl78xx_on_pmc_cfg_script_state_enter(struct hl78xx_data *data) +{ + int ret = 0; + bool modem_require_restart = false; + +#ifdef CONFIG_MODEM_HL78XX_LOW_POWER_MODE + hl78xx_enable_pmc(data); + hl78xx_psm_settings(data); + hl78xx_edrx_settings(data); +#ifdef CONFIG_MODEM_HL78XX_POWER_DOWN + hl78xx_pwr_dwn_settings(data); +#endif +#else + hl78xx_disabe_pmc(data); + LOG_DBG("%d Disabling Power Management Config", __LINE__); +#endif + if (modem_require_restart) { + const char *cmd_restart = (const char *)SET_AIRPLANE_MODE_CMD; + + LOG_DBG("%d Reset required", __LINE__); + + ret = modem_cmd_send_int(data, NULL, cmd_restart, strlen(cmd_restart), &ok_match, 1, + false); + if (ret < 0) { + goto error; + } + hl78xx_start_timer(data, K_MSEC(100)); + return 0; + } +#ifdef CONFIG_MODEM_HL78XX_LOW_POWER_MODE + hl78xx_chat_callback_handler(&data->chat, MODEM_CHAT_SCRIPT_RESULT_SUCCESS, data); +#endif + return 0; +error: + hl78xx_chat_callback_handler(&data->chat, MODEM_CHAT_SCRIPT_RESULT_ABORT, data); + LOG_ERR("Failed to send command: %d", ret); + return ret; +} + +static void hl78xx_run_pmc_cfg_script_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; + + switch (evt) { + case MODEM_HL78XX_EVENT_TIMEOUT: + LOG_DBG("Rebooting modem to apply new RAT settings"); + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SCRIPT_REQUIRE_RESTART); + break; + + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + case MODEM_HL78XX_EVENT_SCRIPT_REQUIRE_RESTART: + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_POWER_ON_PULSE); + break; + } + + if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_RESET_PULSE); + break; + } + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + default: + break; + } +} + +static int hl78xx_on_run_pmc_cfg_script_state_leave(struct hl78xx_data *data) +{ + return 0; +} static int hl78xx_on_await_power_off_state_enter(struct hl78xx_data *data) { @@ -1264,9 +1714,52 @@ static void hl78xx_enable_gprs_event_handler(struct hl78xx_data *data, enum hl78 break; } } +static int hl78xx_on_await_wakeup_state_enter(struct hl78xx_data *data) +{ + hl78xx_start_timer(data, MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT); + return 0; +} + +static void hl78xx_await_wakeup_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_BUS_OPENED: + modem_chat_attach(&data->chat, data->uart_pipe); + break; + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + case MODEM_HL78XX_EVENT_SCRIPT_FAILED: + break; + case MODEM_HL78XX_EVENT_DEVICE_ASLEEP: + break; + case MODEM_HL78XX_EVENT_DEVICE_AWAKE: + hl78xx_stop_timer(data); + modem_pipe_attach(data->uart_pipe, hl78xx_bus_pipe_handler, data); + modem_pipe_open_async(data->uart_pipe); + break; + case MODEM_HL78XX_EVENT_TIMEOUT: + /* If timeout occurs, that means the device could not be waken up for + * MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT, + * so take some action here + */ + break; + + case MODEM_HL78XX_EVENT_REGISTERED: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_CARRIER_ON); + break; + + case MODEM_HL78XX_EVENT_SUSPEND: + hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); + break; + + default: + break; + } +} static int hl78xx_on_await_registered_state_enter(struct hl78xx_data *data) { + /* Start timer to check if psm event received */ + hl78xx_start_timer(data, K_MSEC(MDM_PSMEVT_RECEIVE_TIMEOUT)); return 0; } @@ -1275,14 +1768,36 @@ static void hl78xx_await_registered_event_handler(struct hl78xx_data *data, enum switch (evt) { case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: case MODEM_HL78XX_EVENT_SCRIPT_FAILED: - hl78xx_start_timer(data, MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT); break; case MODEM_HL78XX_EVENT_TIMEOUT: + if (data->status.psm.psmev_current == HL78XX_PSM_EVENT_ENTER) { + /* Just confirm the previous state was registered */ + if (data->status.registration.is_registered_previously) { + LOG_DBG("%d: PSM event received, modem is in PSM mode", __LINE__); + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + } + } else { + if (data->status.registration.is_registered_previously) { + if (data->status.psm.psmev_current == HL78XX_PSM_EVENT_ENTER) { + LOG_DBG("%d: PSM exit requested, waiting for " + "registration", + __LINE__); + } + /* Most likely out of coverage. take a action here */ + } else { + /* There was no previous registration state */ + /* Do nothing wait for it if searching is in progress */ + LOG_DBG("%d waiting for registration : Current network " + "state: %d", + __LINE__, data->status.registration.network_state_current); + } + } break; case MODEM_HL78XX_EVENT_REGISTERED: hl78xx_enter_state(data, MODEM_HL78XX_STATE_CARRIER_ON); + break; case MODEM_HL78XX_EVENT_SUSPEND: @@ -1302,7 +1817,19 @@ static int hl78xx_on_await_registered_state_leave(struct hl78xx_data *data) static int hl78xx_on_carrier_on_state_enter(struct hl78xx_data *data) { - iface_status_work_cb(data, hl78xx_chat_callback_handler); + LOG_DBG("%d %s: Entering carrier on state %d %d %d %d", __LINE__, __func__, + data->status.registration.is_registered_previously, + data->status.registration.is_registered_currently, + data->status.registration.network_state_current, + data->status.registration.network_state_previous); + if (!data->status.psm.psmev_previous && !data->status.psm.psmev_current && + data->status.registration.network_state_previous != HL78XX_REGISTRATION_UNKNOWN) { + iface_status_work_cb(data, hl78xx_chat_callback_handler); + } + + notif_carrier_on(); + LOG_DBG("%d psm %d %d", __LINE__, data->status.psm.psmev_previous, + data->status.psm.psmev_current); return 0; } @@ -1310,20 +1837,42 @@ static void hl78xx_carrier_on_event_handler(struct hl78xx_data *data, enum hl78x { switch (evt) { case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: - hl78xx_start_timer(data, K_SECONDS(2)); + if (!data->status.psm.psmev_previous && !data->status.psm.psmev_current && + data->status.registration.network_state_previous != + HL78XX_REGISTRATION_UNKNOWN) { + hl78xx_start_timer(data, K_SECONDS(2)); + } break; case MODEM_HL78XX_EVENT_SCRIPT_FAILED: break; case MODEM_HL78XX_EVENT_TIMEOUT: - dns_work_cb(); + LOG_DBG("%d %d %d %d", __LINE__, data->status.psm.psmev_previous, + data->status.psm.psmev_current, + data->status.registration.network_state_previous); + LOG_DBG("%d %d %d", __LINE__, data->status.pmc_power_down.requested_previously, + data->status.pmc_power_down.requested_currently); + if (data->status.pmc_power_down.requested_previously && + !data->status.pmc_power_down.requested_currently) { + dns_work_cb(true); + hl78xx_release_socket_comms(); + + } else if ((!data->status.psm.psmev_previous && !data->status.psm.psmev_current && + data->status.registration.network_state_previous != + HL78XX_REGISTRATION_UNKNOWN)) { + dns_work_cb(false); + } else { + LOG_DBG("%d Unexpected condition in carrier on state: %d", __LINE__, evt); + } break; case MODEM_HL78XX_EVENT_DEREGISTERED: hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_REGISTERED); break; case MODEM_HL78XX_EVENT_SUSPEND: + LOG_DBG("%d %s: Modem is suspended, entering power off state", __LINE__, __func__); hl78xx_enter_state(data, MODEM_HL78XX_STATE_INIT_POWER_OFF); break; - + case MODEM_HL78XX_EVENT_SOCKET_CLOSED: + break; default: break; } @@ -1337,12 +1886,14 @@ static int hl78xx_on_carrier_on_state_leave(struct hl78xx_data *data) static int hl78xx_on_carrier_off_state_enter(struct hl78xx_data *data) { + LOG_DBG("%d carrier off", __LINE__); notif_carrier_off(); /* Check whether or not there is any sockets are connected, * if true, wait until sockets are closed properly */ + hl78xx_start_timer(data, K_MSEC(100)); if (check_if_any_socket_connected() == false) { - hl78xx_start_timer(data, K_MSEC(100)); + LOG_DBG("%d THERE is socket connected", __LINE__); } return 0; } @@ -1353,7 +1904,8 @@ static void hl78xx_carrier_off_event_handler(struct hl78xx_data *data, enum hl78 case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: case MODEM_HL78XX_EVENT_SCRIPT_FAILED: case MODEM_HL78XX_EVENT_TIMEOUT: - hl78xx_enter_state(data, MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT); + notif_carrier_on(); + hl78xx_enter_state(data, MODEM_HL78XX_STATE_CARRIER_ON); break; case MODEM_HL78XX_EVENT_DEREGISTERED: hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_REGISTERED); @@ -1374,16 +1926,33 @@ static int hl78xx_on_carrier_off_state_leave(struct hl78xx_data *data) return 0; } +MODEM_CHAT_SCRIPT_CMDS_DEFINE(swir_hl78xx_pwroff_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP(SET_AIRPLANE_MODE_CMD_LEGACY, ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPWROFF", ok_match), + /* MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP?", ksrep_match) */); + +MODEM_CHAT_SCRIPT_DEFINE(swir_hl78xx_pwroff_script, swir_hl78xx_pwroff_cmds, abort_matches, + hl78xx_chat_callback_handler, 4); + static int hl78xx_on_init_power_off_state_enter(struct hl78xx_data *data) { - hl78xx_start_timer(data, K_MSEC(2000)); - return 0; + return modem_chat_run_script_async(&data->chat, &swir_hl78xx_pwroff_script); } static void hl78xx_init_power_off_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) { - if (evt == MODEM_HL78XX_EVENT_TIMEOUT) { - hl78xx_begin_power_off_pulse(data); + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + notif_carrier_off(); + hl78xx_enter_state(data, MODEM_HL78XX_STATE_IDLE); + break; + case MODEM_HL78XX_EVENT_TIMEOUT: + break; + case MODEM_HL78XX_EVENT_DEREGISTERED: + hl78xx_stop_timer(data); + break; + default: + break; } } @@ -1427,13 +1996,12 @@ static int hl78xx_on_idle_state_enter(struct hl78xx_data *data) const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; if (hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { + LOG_DBG("%d: Wake pin is enabled, setting it to low", __LINE__); gpio_pin_set_dt(&config->mdm_gpio_wake, 0); } - if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { - gpio_pin_set_dt(&config->mdm_gpio_reset, 1); - } modem_chat_release(&data->chat); + modem_pipe_attach(data->uart_pipe, hl78xx_bus_pipe_handler, data); modem_pipe_close_async(data->uart_pipe); k_sem_give(&data->suspended_sem); return 0; @@ -1442,9 +2010,37 @@ static int hl78xx_on_idle_state_enter(struct hl78xx_data *data) static void hl78xx_idle_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) { const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; - +#if defined CONFIG_MODEM_HL78XX_LOW_POWER_MODE && defined CONFIG_PM_DEVICE + enum pm_device_state state; + enum pm_device_action action; + int rc; +#endif switch (evt) { + case MODEM_HL78XX_EVENT_BUS_CLOSED: +#if defined CONFIG_MODEM_HL78XX_LOW_POWER_MODE && defined CONFIG_PM_DEVICE + rc = pm_device_state_get(config->uart, &state); + if (rc == 0) { + LOG_DBG("PM state %d ret: %d", state, rc); + } + action = PM_DEVICE_ACTION_SUSPEND; + uart_irq_rx_disable(config->uart); + rc = pm_device_action_run(config->uart, action); + if (rc == 0 || rc == -EALREADY) { + LOG_DBG("PM action run: %d", rc); + } else { + LOG_DBG("PM action run failed: %d", rc); + } +#else + LOG_DBG("%d: Bus closed, entering idle state", __LINE__); +#endif + break; + case MODEM_HL78XX_EVENT_RESUME: + if (data->status.psm.psmev_current == HL78XX_PSM_EVENT_ENTER) { + hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_WAKEUP); + + break; + } if (config->autostarts) { hl78xx_enter_state(data, MODEM_HL78XX_STATE_AWAIT_POWER_ON); break; @@ -1474,7 +2070,11 @@ static void hl78xx_idle_event_handler(struct hl78xx_data *data, enum hl78xx_even static int hl78xx_on_idle_state_leave(struct hl78xx_data *data) { const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; - +#if defined CONFIG_MODEM_HL78XX_LOW_POWER_MODE && defined CONFIG_PM_DEVICE + enum pm_device_state state; + enum pm_device_action action; + int rc = 0; +#endif /* CONFIG_MODEM_HL78XX_LOW_POWER_MODE && CONFIG_PM_DEVICE */ k_sem_take(&data->suspended_sem, K_NO_WAIT); if (hl78xx_gpio_is_enabled(&config->mdm_gpio_reset)) { @@ -1484,14 +2084,31 @@ static int hl78xx_on_idle_state_leave(struct hl78xx_data *data) if (hl78xx_gpio_is_enabled(&config->mdm_gpio_wake)) { gpio_pin_set_dt(&config->mdm_gpio_wake, 1); } - +#if defined CONFIG_MODEM_HL78XX_LOW_POWER_MODE && defined CONFIG_PM_DEVICE + rc = pm_device_state_get(config->uart, &state); + if (rc == 0) { + LOG_DBG("PM state: %d ret: %d", state, rc); + } else { + LOG_DBG("PM state get failed: %d", rc); + } + action = PM_DEVICE_ACTION_RESUME; + rc = pm_device_action_run(config->uart, action); + if (rc == 0 || rc == -EALREADY) { + LOG_DBG("PM action run: %d", rc); + } else { + LOG_DBG("PM action run failed: %d", rc); + } + uart_irq_rx_enable(config->uart); +#endif /* CONFIG_MODEM_HL78XX_LOW_POWER_MODE && CONFIG_PM_DEVICE */ return 0; } static int hl78xx_on_state_enter(struct hl78xx_data *data) { int ret = 0; - +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d %d", __LINE__, data->status.state); +#endif switch (data->status.state) { case MODEM_HL78XX_STATE_IDLE: ret = hl78xx_on_idle_state_enter(data); @@ -1522,11 +2139,18 @@ static int hl78xx_on_state_enter(struct hl78xx_data *data) case MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT: ret = hl78xx_on_rat_cfg_script_state_enter(data); break; + case MODEM_HL78XX_STATE_RUN_PMC_CONFIG_SCRIPT: + ret = hl78xx_on_pmc_cfg_script_state_enter(data); + break; case MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT: ret = hl78xx_on_enable_gprs_state_enter(data); break; + case MODEM_HL78XX_STATE_AWAIT_WAKEUP: + ret = hl78xx_on_await_wakeup_state_enter(data); + break; + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: ret = hl78xx_on_await_registered_state_enter(data); break; @@ -1564,7 +2188,7 @@ static int hl78xx_on_state_leave(struct hl78xx_data *data) int ret = 0; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d", __LINE__, __func__, data->status.state); + LOG_DBG("%d %d", __LINE__, data->status.state); #endif switch (data->status.state) { case MODEM_HL78XX_STATE_IDLE: @@ -1583,6 +2207,10 @@ static int hl78xx_on_state_leave(struct hl78xx_data *data) ret = hl78xx_on_run_rat_cfg_script_state_leave(data); break; + case MODEM_HL78XX_STATE_RUN_PMC_CONFIG_SCRIPT: + ret = hl78xx_on_run_pmc_cfg_script_state_leave(data); + break; + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: ret = hl78xx_on_await_registered_state_leave(data); break; @@ -1631,17 +2259,6 @@ void hl78xx_enter_state(struct hl78xx_data *data, enum hl78xx_state state) } } -static void hl78xx_begin_power_off_pulse(struct hl78xx_data *data) -{ - const struct hl78xx_config *config = (const struct hl78xx_config *)data->dev->config; - - modem_pipe_close_async(data->uart_pipe); - - hl78xx_enter_state(data, hl78xx_gpio_is_enabled(&config->mdm_gpio_pwr_on) - ? MODEM_HL78XX_STATE_POWER_OFF_PULSE - : MODEM_HL78XX_STATE_IDLE); -} - static void hl78xx_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) { enum hl78xx_state state; @@ -1682,10 +2299,18 @@ static void hl78xx_event_handler(struct hl78xx_data *data, enum hl78xx_event evt hl78xx_run_rat_cfg_script_event_handler(data, evt); break; + case MODEM_HL78XX_STATE_RUN_PMC_CONFIG_SCRIPT: + hl78xx_run_pmc_cfg_script_event_handler(data, evt); + break; + case MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT: hl78xx_enable_gprs_event_handler(data, evt); break; + case MODEM_HL78XX_STATE_AWAIT_WAKEUP: + hl78xx_await_wakeup_event_handler(data, evt); + break; + case MODEM_HL78XX_STATE_AWAIT_REGISTERED: hl78xx_await_registered_event_handler(data, evt); break; @@ -1780,6 +2405,9 @@ static int hl78xx_init(const struct device *dev) k_sem_init(&data->suspended_sem, 0, 1); k_sem_init(&data->script_stopped_sem_tx_int, 0, 1); k_sem_init(&data->script_stopped_sem_rx_int, 0, 1); +#ifdef CONFIG_MODEM_HL78XX_POWER_DOWN + k_work_init_delayable(&data->hl78xx_pwr_dwn_work, hl78xx_pwr_dwn_work_handler); +#endif /* reset to default */ data->buffers.eof_pattern_size = strlen(data->buffers.eof_pattern); memset(data->identity.apn, 0, MDM_APN_MAX_LENGTH); @@ -1814,20 +2442,20 @@ static int hl78xx_init(const struct device *dev) gpio_flags_t flags; const char *name; } gpio_config[GPIO_CONFIG_LEN] = { - {&config->mdm_gpio_reset, GPIO_OUTPUT_LOW, "reset"}, - {&config->mdm_gpio_wake, GPIO_OUTPUT_HIGH, "wake"}, + {&config->mdm_gpio_reset, GPIO_OUTPUT, "reset"}, + {&config->mdm_gpio_wake, GPIO_OUTPUT, "wake"}, {&config->mdm_gpio_vgpio, GPIO_INPUT, "VGPIO"}, #if HAS_PWR_ON_GPIO - {&config->mdm_gpio_pwr_on, GPIO_OUTPUT_HIGH, "pwr_on"}, + {&config->mdm_gpio_pwr_on, GPIO_OUTPUT, "pwr_on"}, #endif #if HAS_FAST_SHUTD_GPIO - {&config->mdm_gpio_fast_shtdown, GPIO_OUTPUT_LOW, "fast_shutdown"}, + {&config->mdm_gpio_fast_shtdown, GPIO_OUTPUT, "fast_shutdown"}, #endif #if HAS_UART_DTR_GPIO {&config->mdm_gpio_uart_dtr, GPIO_OUTPUT, "DTR"}, #endif #if HAS_GPIO6_GPIO - {&config->mdm_gpio_gpio6, GPIO_OUTPUT, "GPIO6"}, + {&config->mdm_gpio_gpio6, GPIO_INPUT, "GPIO6"}, #endif }; @@ -1854,7 +2482,24 @@ static int hl78xx_init(const struct device *dev) LOG_ERR("Error configuring VGPIO interrupt! (%d)", ret); goto error; } +#if HAS_GPIO6_GPIO + LOG_DBG("%d %s: GPIO6 is enabled", __LINE__, __func__); + /* GPIO6 interrupt setup */ + gpio_init_callback(&data->gpio_cbs.gpio6_cb, mdm_gpio6_callback_isr, + BIT(config->mdm_gpio_gpio6.pin)); + ret = gpio_add_callback(config->mdm_gpio_gpio6.port, &data->gpio_cbs.gpio6_cb); + if (ret) { + LOG_ERR("Cannot setup GPIO6 callback! (%d)", ret); + goto error; + } + + ret = gpio_pin_interrupt_configure_dt(&config->mdm_gpio_gpio6, GPIO_INT_EDGE_BOTH); + if (ret) { + LOG_ERR("Error configuring GPIO6 interrupt! (%d)", ret); + goto error; + } +#endif /* HAS_GPIO6_GPIO */ (void)hl78xx_init_pipe(dev); ret = modem_init_chat(dev); @@ -1894,7 +2539,7 @@ static DEVICE_API(hl78xx, hl78xx_api) = { }; #define MODEM_HL78XX_DEFINE_INSTANCE(inst, power_ms, reset_ms, startup_ms, shutdown_ms, start, \ - init_script) \ + init_script, periodic_script) \ static const struct hl78xx_config hl78xx_cfg_##inst = { \ .uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \ .mdm_gpio_reset = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_reset_gpios, {}), \ @@ -1914,6 +2559,7 @@ static DEVICE_API(hl78xx, hl78xx_api) = { .shutdown_time_ms = (shutdown_ms), \ .autostarts = (start), \ .init_chat_script = (init_script), \ + .periodic_chat_script = (periodic_script), \ }; \ static struct hl78xx_data hl78xx_data_##inst = { \ .buffers.delimiter = "\r\n", \ @@ -1925,8 +2571,11 @@ static DEVICE_API(hl78xx, hl78xx_api) = { CONFIG_MODEM_HL78XX_DEV_INIT_PRIORITY, &hl78xx_api); #define MODEM_DEVICE_SWIR_HL78XX(inst) \ - MODEM_HL78XX_DEFINE_INSTANCE(inst, 1500, 100, 1000, 1000, false, \ - &swir_hl78xx_init_chat_script) + MODEM_HL78XX_DEFINE_INSTANCE(inst, CONFIG_MODEM_HL78XX_DEV_POWER_PULSE_DURATION, \ + CONFIG_MODEM_HL78XX_DEV_RESET_PULSE_DURATION, \ + CONFIG_MODEM_HL78XX_DEV_STARTUP_TIME, \ + CONFIG_MODEM_HL78XX_DEV_SHUTDOWN_TIME, false, \ + &hl78xx_init_chat_script, &hl78xx_periodic_chat_script) #define DT_DRV_COMPAT swir_hl7812 DT_INST_FOREACH_STATUS_OKAY(MODEM_DEVICE_SWIR_HL78XX) diff --git a/drivers/modem/hl78xx/hl78xx.h b/drivers/modem/hl78xx/hl78xx.h index 2464d00a26aa..708afce81837 100644 --- a/drivers/modem/hl78xx/hl78xx.h +++ b/drivers/modem/hl78xx/hl78xx.h @@ -36,6 +36,7 @@ #define MDM_RESET_HIGH_TIME (10) /*K_MSEC*/ #define MDM_BOOT_TIME (12) /*K_SECONDS*/ #define MDM_DNS_ADD_TIMEOUT (100) /*K_MSEC*/ +#define MDM_PSMEVT_RECEIVE_TIMEOUT (100) /*K_MSEC*/ #define MODEM_HL78XX_PERIODIC_SCRIPT_TIMEOUT K_MSEC(CONFIG_MODEM_HL78XX_PERIODIC_SCRIPT_MS) #define MDM_MAX_DATA_LENGTH CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES @@ -74,12 +75,14 @@ #else #define MODEM_HL78XX_ADDRESS_FAMILY ADDRESS_FAMILY_IPV6 #endif +#define HL78XX_ACT_TYPE_RAT_MASK 4 /* Modem Communication Patterns */ #define EOF_PATTERN "--EOF--Pattern--" #define EOF_PATTERN_GNSS "+++" #define CONNECT_STRING "CONNECT" -#define OK_STRING "OK" +#define CME_ERROR_STRING "+CME ERROR: " +#define OK_STRING "OK" /* RAT (Radio Access Technology) commands */ #define SET_RAT_M1_CMD_LEGACY "AT+KSRAT=0" @@ -100,6 +103,8 @@ #define SET_AIRPLANE_MODE_CMD "AT+CFUN=4,1" #define SET_FULLFUNCTIONAL_MODE_CMD_LEGACY "AT+CFUN=1,0" #define SET_FULLFUNCTIONAL_MODE_CMD "AT+CFUN=1,1" +#define MDM_POWER_OFF_CMD_LEGACY "AT+CPWROFF" +#define MDM_POWER_FAST_OFF_CMD_LEGACY "AT+CPWROFF=1" #define GET_FULLFUNCTIONAL_MODE_CMD "AT+CFUN?" /* PDP Context commands */ #define DEACTIVATE_PDP_CONTEXT "AT+CGACT=0" @@ -107,6 +112,7 @@ /* Helper macros */ #define ATOI(s_, value_, desc_) modem_atoi(s_, value_, desc_, __func__) +#define ATOD(s_, value_, desc_) modem_atod(s_, value_, desc_, __func__) /* Enums */ @@ -143,11 +149,13 @@ enum hl78xx_state { MODEM_HL78XX_STATE_RUN_INIT_SCRIPT, MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT, MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT, + MODEM_HL78XX_STATE_RUN_PMC_CONFIG_SCRIPT, MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT, /* Full functionality, searching * CFUN=1 */ MODEM_HL78XX_STATE_AWAIT_REGISTERED, + MODEM_HL78XX_STATE_AWAIT_WAKEUP, MODEM_HL78XX_STATE_CARRIER_ON, /* Minimum functionality, SIM powered off, Modem Power down * CFUN=0 @@ -176,13 +184,56 @@ enum hl78xx_event { MODEM_HL78XX_EVENT_BUS_OPENED, MODEM_HL78XX_EVENT_BUS_CLOSED, MODEM_HL78XX_EVENT_SOCKET_READY, + MODEM_HL78XX_EVENT_SOCKET_CLOSED, + MODEM_HL78XX_EVENT_DEVICE_AWAKE, + MODEM_HL78XX_EVENT_DEVICE_ASLEEP, }; + +enum hl78xx_pmc_modes { + HL78XX_PMC_MODE_EDRX, + HL78XX_PMC_MODE_PSM, + HL78XX_PMC_MODE_POWER_DOWN, + HL78XX_PMC_MODE_NONE +}; + +enum hl78xx_tcp_notif { + TCP_NOTIF_NETWORK_ERROR = 0, + TCP_NOTIF_NO_MORE_SOCKETS = 1, + TCP_NOTIF_MEMORY_PROBLEM = 2, + TCP_NOTIF_DNS_ERROR = 3, + TCP_NOTIF_REMOTE_DISCONNECTION = 4, + TCP_NOTIF_CONNECTION_ERROR = 5, + TCP_NOTIF_GENERIC_ERROR = 6, + TCP_NOTIF_ACCEPT_FAILED = 7, + TCP_NOTIF_SEND_MISMATCH = 8, + TCP_NOTIF_BAD_SESSION_ID = 9, + TCP_NOTIF_SESSION_ALREADY_RUNNING = 10, + TCP_NOTIF_ALL_SESSIONS_USED = 11, + TCP_NOTIF_CONNECTION_TIMEOUT = 12, + TCP_NOTIF_SSL_CONNECTION_ERROR = 13, + TCP_NOTIF_SSL_INIT_ERROR = 14, + TCP_NOTIF_SSL_CERT_ERROR = 15 +}; + +enum hl78xx_kedrx_mode { + HL78XX_KEDRX_MODE_DISABLE = 0, + HL78XX_KEDRX_MODE_ENABLE = 1, + HL78XX_KEDRX_MODE_ENABLE_W_URC = 2, + HL78XX_KEDRX_MODE_DISABLE_AND_ERASE_CFG = 3, +}; + +enum hl78xx_kedrx_ack_type { + HL78XX_KEDRX_ACK_TYPE_CATM = 4, + HL78XX_KEDRX_ACK_TYPE_NB = 5, +}; + struct kselacq_syntax { bool mode; enum hl78xx_cell_rat_mode rat1; enum hl78xx_cell_rat_mode rat2; enum hl78xx_cell_rat_mode rat3; }; + struct kband_syntax { uint8_t rat; /* Max 64 digits representation format is supported @@ -195,11 +246,46 @@ struct kband_syntax { uint8_t bnd_bitmap[MDM_BAND_HEX_STR_LEN]; }; +struct ksleep_syntax { + uint8_t mngt; + uint8_t level; + uint8_t delay; +}; + +struct power_down_syntax { + bool requested_previously; + bool requested_currently; + bool status_currently; + bool status_previously; +}; + +struct cpsms_syntax { + /* Indication to disable or enable the use of PSM in the UE; */ + bool mode; + /* TAU value (T3412) */ + uint8_t periodic_tau; + /* Active Time value (T3324) */ + uint8_t active_time; +}; + +struct kedrxcfg_syntax { + enum hl78xx_kedrx_mode mode; + enum hl78xx_kedrx_ack_type ack_type; + uint8_t requested_edrx; +}; + struct registration_status { - bool is_registered; - enum hl78xx_registration_status network_state; + bool is_registered_currently; + bool is_registered_previously; + enum hl78xx_registration_status network_state_current; + enum hl78xx_registration_status network_state_previous; enum hl78xx_cell_rat_mode rat_mode; }; +struct hl78xx_psm_status { + enum hl78xx_psmev_event psmev_current; + enum hl78xx_psmev_event psmev_previous; + bool is_psm_active; +}; /* driver data */ struct modem_buffers { uint8_t uart_rx[CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES]; @@ -224,15 +310,21 @@ struct modem_identity { struct modem_status { struct registration_status registration; - uint8_t rssi; + int16_t rssi; uint8_t ksrep; - uint8_t rsrp; - uint8_t rsrq; + int16_t rsrp; + int16_t rsrq; uint16_t script_fail_counter; int variant; enum hl78xx_state state; - struct kband_syntax kbndcfg[HL78XX_RAT_COUNT]; enum hl78xx_phone_functionality phone_functionality; + struct hl78xx_psm_status psm; + struct kband_syntax kbndcfg[HL78XX_RAT_COUNT]; + /* Power Management Control */ + struct ksleep_syntax pmc_sleep; + struct cpsms_syntax pmc_cpsms; + struct kedrxcfg_syntax pmc_kedrxcfg[2]; + struct power_down_syntax pmc_power_down; }; struct modem_gpio_callbacks { @@ -266,7 +358,9 @@ struct hl78xx_data { struct modem_event_system events; struct k_work_delayable timeout_work; - +#ifdef CONFIG_MODEM_HL78XX_POWER_DOWN + struct k_work_delayable hl78xx_pwr_dwn_work; +#endif #if defined(CONFIG_MODEM_HL78XX_RSSI_WORK) struct k_work_delayable rssi_query_work; #endif @@ -296,6 +390,7 @@ struct hl78xx_config { bool autostarts; const struct modem_chat_script *init_chat_script; + const struct modem_chat_script *periodic_chat_script; }; /* socket read callback data */ struct socket_read_data { @@ -333,9 +428,10 @@ uint32_t hash32(const char *str, int len); /** * @brief DNS resolution work callback. * + * @param reset If true, resets the DNS resolver state. * Should be used internally to handle DNS resolution events. */ -void dns_work_cb(void); +void dns_work_cb(bool reset); /** * @brief Callback to update and handle network interface status. @@ -363,6 +459,20 @@ void iface_status_work_cb(struct hl78xx_data *data, */ int modem_atoi(const char *s, const int err_value, const char *desc, const char *func); +/** + * @brief Convert a string to an double with error handling. + * + * Similar to atoi, but allows specifying an error fallback and logs errors. + * + * @param s Input string to convert. + * @param err_value Value to return on failure. + * @param desc Description of the value for logging purposes. + * @param func Function name for logging purposes. + * + * @return Converted double on success, or err_value on failure. + */ +double modem_atod(const char *s, const double err_value, const char *desc, const char *func); + /** * @brief Initialize sockets for the modem. * @@ -380,7 +490,17 @@ void hl78xx_socket_init(struct hl78xx_data *data); * @param socket_id ID of the affected socket. * @param new_total New data count or buffer level associated with the socket. */ -void socknotifydata(int socket_id, int new_total); +void socket_notify_data(int socket_id, int new_total); + +/** + * @brief Notify the system of tcp socket changes. + * + * Typically used when tcp connection failure has been received on a socket. + * + * @param socket_id ID of the affected socket. + * @param tcp_notif Integer type. Indicates the cause of the TCP connection failure. + */ +void tcp_notify_data(int socket_id, int tcp_notif); /** * @brief Send a command to the modem and wait for matching response(s). @@ -519,40 +639,177 @@ void hl78xx_bitmap_to_hex_string_trimmed(const uint8_t *bitmap, char *hex_str, s * @retval 0 on success. * @retval Negative errno code on failure (e.g., invalid characters, overflow). */ +/** + * @brief hl78xx_hex_string_to_bitmap - Brief description of the function. + * @param hex_str Description of hex_str. + * @param bitmap_out Description of bitmap_out. + * @return int Description of return value. + */ int hl78xx_hex_string_to_bitmap(const char *hex_str, uint8_t *bitmap_out); +/** + * @brief hl78xx_extract_essential_part_apn - Brief description of the function. + * @param full_apn Description of full_apn. + * @param essential_apn Description of essential_apn. + * @param max_len Description of max_len. + */ void hl78xx_extract_essential_part_apn(const char *full_apn, char *essential_apn, size_t max_len); +/** + * @brief hl78xx_set_apn_internal - Brief description of the function. + * @param data Description of data. + * @param apn Description of apn. + * @param size Description of size. + * @return int Description of return value. + */ int hl78xx_set_apn_internal(struct hl78xx_data *data, const char *apn, uint16_t size); +/** + * @brief hl78xx_api_func_set_phone_functionality - Brief description of the function. + * @param dev Description of dev. + * @param functionality Description of functionality. + * @param reset Description of reset. + * @return int Description of return value. + */ int hl78xx_api_func_set_phone_functionality(const struct device *dev, enum hl78xx_phone_functionality functionality, bool reset); +/** + * @brief hl78xx_api_func_get_phone_functionality - Brief description of the function. + * @param dev Description of dev. + * @param functionality Description of functionality. + * @return int Description of return value. + */ int hl78xx_api_func_get_phone_functionality(const struct device *dev, enum hl78xx_phone_functionality *functionality); +/** + * @brief hl78xx_api_func_get_signal - Brief description of the function. + * @param dev Description of dev. + * @param type Description of type. + * @param value Description of value. + * @return int Description of return value. + */ int hl78xx_api_func_get_signal(const struct device *dev, const enum hl78xx_signal_type type, int16_t *value); +/** + * @brief hl78xx_api_func_get_registration_status - Brief description of the function. + * @param dev Description of dev. + * @param tech Description of tech. + * @param status Description of status. + * @return int Description of return value. + */ int hl78xx_api_func_get_registration_status(const struct device *dev, enum hl78xx_cell_rat_mode *tech, enum hl78xx_registration_status *status); +/** + * @brief hl78xx_api_func_get_modem_info - Brief description of the function. + * @param dev Description of dev. + * @param type Description of type. + * @param info Description of info. + * @param size Description of size. + * @return int Description of return value. + */ int hl78xx_api_func_get_modem_info(const struct device *dev, enum hl78xx_modem_info_type type, char *info, size_t size); +/** + * @brief hl78xx_api_func_set_apn - Brief description of the function. + * @param dev Description of dev. + * @param apn Description of apn. + * @param size Description of size. + * @return int Description of return value. + */ int hl78xx_api_func_set_apn(const struct device *dev, const char *apn, uint16_t size); +/** + * @brief hl78xx_api_func_modem_cmd_send_int - Brief description of the function. + * @param dev Description of dev. + * @param cmd Description of cmd. + * @param cmd_size Description of cmd_size. + * @param response_matches Description of response_matches. + * @param matches_size Description of matches_size. + * @return int Description of return value. + */ int hl78xx_api_func_modem_cmd_send_int(const struct device *dev, const char *cmd, uint16_t cmd_size, const struct modem_chat_match *response_matches, uint16_t matches_size); +/** + * @brief hl78xx_enter_state - Brief description of the function. + * @param data Description of data. + * @param state Description of state. + */ void hl78xx_enter_state(struct hl78xx_data *data, enum hl78xx_state state); +/** + * @brief hl78xx_delegate_event - Brief description of the function. + * @param data Description of data. + * @param evt Description of evt. + */ void hl78xx_delegate_event(struct hl78xx_data *data, enum hl78xx_event evt); +/** + * @brief notif_carrier_off - Brief description of the function. + * @param void Description of void. + */ void notif_carrier_off(void); + +/** + * @brief notif_carrier_on - Brief description of the function. + * @param void Description of void. + */ +void notif_carrier_on(void); + +/** + * @brief check_if_any_socket_connected - Brief description of the function. + * @param void Description of void. + * @return int Description of return value. + */ int check_if_any_socket_connected(void); +/** + * @brief binary_str_to_byte - Brief description of the function. + * @param bin_str Description of bin_str. + * @return int Description of return value. + */ +int binary_str_to_byte(const char *bin_str); + +/** + * @brief byte_to_binary_str - Brief description of the function. + * @param byte Description of byte. + * @param output Description of output. + */ +void byte_to_binary_str(uint8_t byte, char *output); + +/** + * @brief hl78xx_release_socket_comms - Brief description of the function. + * @param void Description of void. + */ +void hl78xx_release_socket_comms(void); + +/** + * @brief hl78xx_is_in_psm - Brief description of the function. + * @param data Description of data. + * @return bool Description of return value. + */ +bool hl78xx_is_in_psm(struct hl78xx_data *data); + +/** + * @brief hl78xx_is_in_pwr_dwn - Brief description of the function. + * @param data Description of data. + * @return bool Description of return value. + */ +bool hl78xx_is_in_pwr_dwn(struct hl78xx_data *data); + +/** + * @brief hl78xx_is_rsrp_valid - Brief description of the function. + * @param data Description of data. + * @return bool Description of return value. + */ +bool hl78xx_is_rsrp_valid(struct hl78xx_data *data); + #endif /* HL78XX_H */ diff --git a/drivers/modem/hl78xx/hl78xx_apis.c b/drivers/modem/hl78xx/hl78xx_apis.c index 07c60188e37a..8d798a8919ad 100644 --- a/drivers/modem/hl78xx/hl78xx_apis.c +++ b/drivers/modem/hl78xx/hl78xx_apis.c @@ -121,7 +121,7 @@ int hl78xx_api_func_get_registration_status(const struct device *dev, } *tech = data->status.registration.rat_mode; - *status = data->status.registration.network_state; + *status = data->status.registration.network_state_current; return 0; } diff --git a/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor b/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor index e002f24ec2c1..26547fe3b579 100644 --- a/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/Kconfig.hl78xx_evt_monitor @@ -5,7 +5,7 @@ # menuconfig HL78XX_EVT_MONITOR - bool "HL78XX AT notification monitor" + bool "HL78XX event notification monitor" if HL78XX_EVT_MONITOR diff --git a/drivers/modem/hl78xx/hl78xx_sockets.c b/drivers/modem/hl78xx/hl78xx_sockets.c index 7c72738a3258..360b73d7ef26 100644 --- a/drivers/modem/hl78xx/hl78xx_sockets.c +++ b/drivers/modem/hl78xx/hl78xx_sockets.c @@ -15,6 +15,9 @@ #include #include #include +#include +#include + #include #include #include "hl78xx.h" @@ -22,9 +25,6 @@ LOG_MODULE_REGISTER(hl78xx_socket, CONFIG_MODEM_LOG_LEVEL); /* Helper macros and constants */ -#define CGCONTRDP_RESPONSE_NUM_DELIMS 7 -#define MDM_IP_INFO_RESP_SIZE 256 - #define MODEM_STREAM_STARTER_WORD "\r\n" CONNECT_STRING "\r\n" #define MODEM_STREAM_END_WORD "\r\n" OK_STRING "\r\n" @@ -34,12 +34,18 @@ LOG_MODULE_REGISTER(hl78xx_socket, CONFIG_MODEM_LOG_LEVEL); (0 + (IS_ENABLED(CONFIG_NET_IPV6) ? 1 : 0) + (IS_ENABLED(CONFIG_NET_IPV4) ? 1 : 0) + \ 1 /* for NULL terminator */ \ ) +#define L4_EVENT_MASK (NET_EVENT_DNS_SERVER_ADD | NET_EVENT_L4_DISCONNECTED) +#define CONN_LAYER_EVENT_MASK (NET_EVENT_CONN_IF_FATAL_ERROR) + +#define HL78XX_UART_PIPE_WORK_SOCKET_BUFFER_SIZE 32 +#define HL78XX_MAC_ADDR_SIZE 6 + RING_BUF_DECLARE(mdm_recv_pool, CONFIG_MODEM_HL78XX_UART_BUFFER_SIZES); /* ---------------- Global Data Structures ---------------- */ struct hl78xx_socket_data { struct net_if *net_iface; - uint8_t mac_addr[6]; + uint8_t mac_addr[HL78XX_MAC_ADDR_SIZE]; /* socket data */ struct modem_socket_config socket_config; @@ -63,9 +69,16 @@ struct hl78xx_socket_data { uint32_t collected_buf_len; struct hl78xx_data *mdata_global; + + struct net_mgmt_event_callback l4_cb; + struct net_mgmt_event_callback conn_cb; + + struct k_sem psm_cntrl_sem; + + bool socket_data_error; }; struct work_socket_data { - char buf[32]; + char buf[HL78XX_UART_PIPE_WORK_SOCKET_BUFFER_SIZE]; uint16_t len; }; @@ -75,7 +88,6 @@ struct receive_socket_data { uint16_t len; }; -uint8_t *buf_argv[32]; struct work_socket_data work_buf; struct receive_socket_data receive_buf; @@ -92,47 +104,7 @@ atomic_t state_leftover; struct hl78xx_socket_data socket_data; static int offload_socket(int family, int type, int protom); - -static void hl78xx_on_kudpsnd(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) -{ - struct modem_socket *sock = NULL; - int id; - - /* look up new socket by special id */ - sock = modem_socket_from_newid(&socket_data.socket_config); - if (sock) { - id = ATOI(argv[1], -1, "socket_id"); - sock->is_connected = true; - - /* on error give up modem socket */ - if (modem_socket_id_assign(&socket_data.socket_config, sock, id) < 0) { - - modem_socket_put(&socket_data.socket_config, sock->sock_fd); - } - } -} - -/* Handler: +USOCR: [0] */ -static void hl78xx_on_cmd_sockcreate(struct modem_chat *chat, char **argv, uint16_t argc, - void *user_data) -{ - struct modem_socket *sock = NULL; - int socket_id; - -#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); -#endif - /* look up new socket by special id */ - sock = modem_socket_from_newid(&socket_data.socket_config); - if (sock) { - socket_id = ATOI(argv[1], -1, "socket_id"); - /* on error give up modem socket */ - if (modem_socket_id_assign(&socket_data.socket_config, sock, socket_id) < 0) { - modem_socket_put(&socket_data.socket_config, sock->sock_fd); - } - } - /* don't give back semaphore -- OK to follow */ -} +static int socket_close(struct modem_socket *sock); void hl78xx_on_kstatev_parser(struct hl78xx_data *data, int state) { @@ -296,6 +268,71 @@ static bool split_ipv4_and_subnet(const char *combined, char *ip_out, size_t ip_ return true; } +void hl78xx_release_socket_comms(void) +{ + k_sem_give(&socket_data.psm_cntrl_sem); +} +static void hl78xx_send_wakeup_signal(void) +{ + hl78xx_delegate_event(socket_data.mdata_global, MODEM_HL78XX_EVENT_RESUME); +} + +static int validate_socket(const struct modem_socket *sock) +{ + + if (!sock) { + return -EINVAL; + } + + if (!sock->is_connected && sock->ip_proto != IPPROTO_UDP) { + errno = ENOTCONN; + return -1; + } + + return 0; +} + +static void hl78xx_on_kudpsnd(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct modem_socket *sock = NULL; + int id; + + /* look up new socket by special id */ + sock = modem_socket_from_newid(&socket_data.socket_config); + if (sock) { + id = ATOI(argv[1], -1, "socket_id"); + sock->is_connected = true; + + /* on error give up modem socket */ + if (modem_socket_id_assign(&socket_data.socket_config, sock, id) < 0) { + + modem_socket_put(&socket_data.socket_config, sock->sock_fd); + } + } +} + +/* Handler: +USOCR: [0] */ +static void hl78xx_on_cmd_sockcreate(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct modem_socket *sock = NULL; + int socket_id; + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d", __LINE__); +#endif + /* look up new socket by special id */ + sock = modem_socket_from_newid(&socket_data.socket_config); + if (sock) { + socket_id = ATOI(argv[1], -1, "socket_id"); + /* on error give up modem socket */ + if (modem_socket_id_assign(&socket_data.socket_config, sock, socket_id) < 0) { + modem_socket_put(&socket_data.socket_config, sock->sock_fd); + } + } + /* don't give back semaphore -- OK to follow */ +} + static void hl78xx_on_cgdcontrdp(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) { @@ -370,20 +407,39 @@ static void hl78xx_on_ktcpstate(struct modem_chat *chat, char **argv, uint16_t a LOG_DBG("%d %s %s %s %s %s", __LINE__, argv[1], argv[2], argv[3], argv[4], argv[5]); #endif uint8_t tcp_session_id = ATOI(argv[1], 0, "tcp_session_id"); - uint8_t tcp_status = ATOI(argv[2], 0, "tcp_status"); - int8_t tcp_notif = ATOI(argv[3], 0, "tcp_notif"); + uint8_t tcp_status = ATOI(argv[2], -1, "tcp_status"); + int8_t tcp_notif = ATOI(argv[3], -1, "tcp_notif"); uint16_t rcv_data = ATOI(argv[5], 0, "tcp_rcv_data"); if (tcp_status != 3 && tcp_notif != -1) { return; } - socknotifydata(tcp_session_id, rcv_data); + socket_notify_data(tcp_session_id, rcv_data); } +static void hl78xx_on_cme_error(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + if (argc < 2 || !argv[1]) { + LOG_ERR("CME ERROR: Incomplete response"); + return; + } + + int error_code = ATOI(argv[1], -1, "CME error code"); + + if (error_code < 0) { + LOG_ERR("Invalid CME error code: %d", error_code); + return; + } + socket_data.socket_data_error = true; + LOG_ERR("CME ERROR: %d", error_code); +} MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", NULL); MODEM_CHAT_MATCHES_DEFINE(connect_matches, MODEM_CHAT_MATCH(CONNECT_STRING, "", NULL), - MODEM_CHAT_MATCH("ERROR", "", NULL)); + MODEM_CHAT_MATCH("+CME ERROR: ", "", hl78xx_on_cme_error)); +MODEM_CHAT_MATCHES_DEFINE(allow_matches, MODEM_CHAT_MATCH("OK", "", NULL), + MODEM_CHAT_MATCH("+CME ERROR: ", "", NULL)); MODEM_CHAT_MATCH_DEFINE(kudpind_match, "+KUDP_IND: ", ",", hl78xx_on_kudpsnd); MODEM_CHAT_MATCH_DEFINE(ktcpind_match, "+KTCP_IND: ", ",", hl78xx_on_kudpsnd); MODEM_CHAT_MATCH_DEFINE(ktcp_match, "+KTCPCFG: ", "", hl78xx_on_cmd_sockcreate); @@ -482,9 +538,20 @@ static void try_handle_eof_pattern(void) LOG_ERR("ring_buf_put failed: %d", ret); } socket_data_received = true; + socket_data.collected_buf_len += ret; + LOG_DBG("%d %d bytes received, total collected: %d %d", __LINE__, ret, + socket_data.collected_buf_len, start_index_eof); } } +static bool is_cme_error_match(void) +{ + uint8_t size_match = strlen(CME_ERROR_STRING); + + return receive_buf.len >= size_match && + modem_chat_match_matches_received(CME_ERROR_STRING, size_match); +} + static bool is_connect_match(void) { uint8_t size_match = strlen(CONNECT_STRING); @@ -532,12 +599,24 @@ static void socket_process_bytes(char byte) return; } - if (!match_found && !match_connect_found && is_connect_match()) { - match_connect_found = true; - LOG_DBG("CONNECT matched. Expecting %d more bytes.", socket_data.expected_buf_len); - return; + if (!match_found && !match_connect_found) { + if (is_connect_match()) { + match_connect_found = true; + LOG_DBG("CONNECT matched. Expecting %d more bytes.", + socket_data.expected_buf_len); + return; + } else if (is_cme_error_match()) { + match_found = true; /* mark this to prevent further parsing */ + LOG_ERR("CME ERROR received. Connection failed."); + socket_data.expected_buf_len = 0; + socket_data.collected_buf_len = 0; + parser_reset(); + socket_data.socket_data_error = true; + /* Optionally notify script or raise an event here */ + k_sem_give(&socket_data.mdata_global->script_stopped_sem_rx_int); + return; + } } - if (match_connect_found && !match_eof_ok_found && is_ok_match()) { match_eof_ok_found = true; LOG_DBG("OK matched."); @@ -581,7 +660,7 @@ static void modem_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event e void *user_data) { #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s Pipe event received: %d", __LINE__, __func__, event); + LOG_DBG("%d Pipe event received: %d", __LINE__, event); #endif switch (event) { case MODEM_PIPE_EVENT_RECEIVE_READY: @@ -603,6 +682,11 @@ void notif_carrier_off(void) net_if_carrier_off(socket_data.net_iface); } +void notif_carrier_on(void) +{ + net_if_carrier_on(socket_data.net_iface); +} + void iface_status_work_cb(struct hl78xx_data *data, modem_chat_script_callback script_user_callback) { @@ -617,7 +701,7 @@ void iface_status_work_cb(struct hl78xx_data *data, modem_chat_script_callback s } } -void dns_work_cb(void) +void dns_work_cb(bool reset) { #if defined(CONFIG_DNS_RESOLVER) && !defined(CONFIG_DNS_SERVER_IP_ADDRESSES) int ret; @@ -635,6 +719,15 @@ void dns_work_cb(void) NULL}; const char *dns_servers_wrapped[ARRAY_SIZE(dns_servers_str)]; + if (reset) { + LOG_DBG("Resetting DNS resolver"); + dnsCtx = dns_resolve_get_default(); + if (dnsCtx->state != DNS_RESOLVE_CONTEXT_INACTIVE) { + dns_resolve_close(dnsCtx); + } + socket_data.dns_ready = false; + } + #ifdef CONFIG_NET_IPV6 valid_address = net_ipaddr_parse(socket_data.dns_v6_string, strlen(socket_data.dns_v6_string), &temp_addr); @@ -668,7 +761,7 @@ void dns_work_cb(void) ret = dns_resolve_init(dnsCtx, dns_servers_wrapped, NULL); } else { LOG_DBG("Reconfiguring DNS resolver"); - ret = dns_resolve_reconfigure(dnsCtx, dns_servers_wrapped, NULL); + ret = dns_resolve_reconfigure(dnsCtx, dns_servers_wrapped, NULL, DNS_SOURCE_MANUAL); } if (ret < 0) { LOG_ERR("dns_resolve_reconfigure fail (%d)", ret); @@ -683,7 +776,7 @@ void dns_work_cb(void) #endif } -static int on_cmd_sockread_common(int socket_id, int socket_data_length, uint16_t len, +static int on_cmd_sockread_common(int socket_id, uint16_t socket_data_length, uint16_t len, void *user_data) { struct modem_socket *sock; @@ -702,9 +795,14 @@ static int on_cmd_sockread_common(int socket_id, int socket_data_length, uint16_ LOG_ERR("Socket data missing! Ignoring (%d)", socket_id); return -EINVAL; } + if (socket_data.socket_data_error && socket_data.collected_buf_len == 0) { + errno = ECONNABORTED; + return -ECONNABORTED; + } - if (!len || socket_data_length <= 0) { - LOG_ERR("Invalid data length: %d. Aborting!", socket_data_length); + if ((len <= 0) || socket_data_length <= 0 || socket_data.collected_buf_len < (size_t)len) { + LOG_ERR("%d Invalid data length: %d %d %d Aborting!", __LINE__, socket_data_length, + (int)len, socket_data.collected_buf_len); return -EAGAIN; } @@ -716,30 +814,29 @@ static int on_cmd_sockread_common(int socket_id, int socket_data_length, uint16_ ret = ring_buf_get(socket_data.buf_pool, sock_data->recv_buf, len); if (ret != len) { - LOG_ERR("Data retrieval mismatch: expected %zu, got %d", len, ret); + LOG_ERR("Data retrieval mismatch: expected %u, got %d", len, ret); return -EAGAIN; } #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG LOG_HEXDUMP_DBG(sock_data->recv_buf, ret, "Received Data:"); #endif - if (sock_data->recv_buf_len < len) { + if (sock_data->recv_buf_len < (size_t)len) { LOG_ERR("Buffer overflow! Received: %zu vs. Available: %zu", len, sock_data->recv_buf_len); return -EINVAL; } - sock_data->recv_read_len = len; - - if (sock_data->recv_read_len != socket_data_length) { + if ((size_t)len != (size_t)socket_data_length) { LOG_ERR("Data mismatch! Copied: %zu vs. Received: %d", len, socket_data_length); return -EINVAL; } + sock_data->recv_read_len = len; + /* Remove packet from list */ packet_size = modem_socket_next_packet_size(&socket_data.socket_config, sock); - modem_socket_packet_size_update(&socket_data.socket_config, sock, -packet_size); - ring_buf_reset(socket_data.buf_pool); + modem_socket_packet_size_update(&socket_data.socket_config, sock, -socket_data_length); socket_data.collected_buf_len = 0; return len; @@ -759,7 +856,7 @@ static int create_socket(struct modem_socket *sock, const struct sockaddr *addr, struct hl78xx_data *data) { #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif int ret; int af; @@ -822,10 +919,50 @@ static int create_socket(struct modem_socket *sock, const struct sockaddr *addr, error: LOG_ERR("%s ret:%d", cmd_buf, ret); modem_socket_put(&socket_data.socket_config, sock->sock_fd); - errno = -ret; + errno = ret; return -1; } +static int socket_close(struct modem_socket *sock) +{ + char buf[sizeof("AT+KTCPCLOSE=##\r")]; + int ret = 0; + + if (sock->ip_proto == IPPROTO_UDP) { + snprintk(buf, sizeof(buf), "AT+KUDPCLOSE=%d", sock->id); + } else { + snprintk(buf, sizeof(buf), "AT+KTCPCLOSE=%d", sock->id); + } + + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), allow_matches, 2, + false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + } + + return ret; +} + +static int socket_delete(struct modem_socket *sock) +{ + char buf[sizeof("AT+KTCPDEL=##\r")]; + int ret = 0; + + if (sock->ip_proto == IPPROTO_UDP) { + snprintk(buf, sizeof(buf), "AT+KUDPDEL=%d", sock->id); + } else { + snprintk(buf, sizeof(buf), "AT+KTCPDEL=%d", sock->id); + } + + ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), allow_matches, 2, + false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + } + + return ret; +} + /* * Socket Offload OPS */ @@ -836,7 +973,7 @@ static int offload_socket(int family, int type, int proto) /* defer modem's socket create call to bind() */ ret = modem_socket_get(&socket_data.socket_config, family, type, proto); if (ret < 0) { - errno = -ret; + errno = ret; return -1; } @@ -847,41 +984,23 @@ static int offload_socket(int family, int type, int proto) static int offload_close(void *obj) { struct modem_socket *sock = (struct modem_socket *)obj; - char buf[sizeof("AT+KTCPCLOSE=##\r")]; - int ret; - - if (!hl78xx_is_registered(socket_data.mdata_global)) { - LOG_ERR("Modem currently not attached to the network!"); - return -EAGAIN; - } /* make sure socket is allocated and assigned an id */ if (modem_socket_id_is_assigned(&socket_data.socket_config, sock) == false) { return 0; } - - if (sock->is_connected) { - if (sock->ip_proto == IPPROTO_UDP) { - snprintk(buf, sizeof(buf), "AT+KUDPCLOSE=%d", sock->id); - - ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), - NULL, 0, false); - if (ret < 0) { - LOG_ERR("%s ret:%d", buf, ret); - } - } else { - snprintk(buf, sizeof(buf), "AT+KTCPCLOSE=%d", sock->id); - ret = modem_cmd_send_int(socket_data.mdata_global, NULL, buf, strlen(buf), - NULL, 0, false); - if (ret < 0) { - LOG_ERR("%s ret:%d", buf, ret); - } + if (validate_socket(sock) == 0) { + LOG_DBG("%d %d %d", __LINE__, + socket_data.mdata_global->status.pmc_power_down.requested_previously, + socket_data.mdata_global->status.pmc_power_down.status_previously); + if (!socket_data.mdata_global->status.pmc_power_down.requested_previously && + !socket_data.mdata_global->status.pmc_power_down.status_previously) { + socket_close(sock); + socket_delete(sock); } + modem_socket_put(&socket_data.socket_config, sock->sock_fd); sock->is_connected = false; } - - modem_socket_put(&socket_data.socket_config, sock->sock_fd); /* Consider here successfully socket is closed */ - hl78xx_delegate_event(socket_data.mdata_global, MODEM_HL78XX_EVENT_SCRIPT_SUCCESS); return 0; } @@ -890,7 +1009,7 @@ static int offload_bind(void *obj, const struct sockaddr *addr, socklen_t addrle struct modem_socket *sock = (struct modem_socket *)obj; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif /* Save bind address information */ memcpy(&sock->src, addr, sizeof(*addr)); @@ -916,7 +1035,7 @@ static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t add uint16_t dst_port = 0U; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif if (!addr) { errno = EINVAL; @@ -926,7 +1045,8 @@ static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t add errno = ENETUNREACH; return -1; } - if (sock->is_connected == true) { + + if (validate_socket(sock) == 0) { LOG_ERR("Socket is already connected! id: %d, fd: %d", sock->id, sock->sock_fd); errno = EISCONN; return -1; @@ -942,7 +1062,7 @@ static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t add /* make sure we've created the socket */ if (modem_socket_id_is_assigned(&socket_data.socket_config, sock) == false && sock->ip_proto == IPPROTO_UDP) { - LOG_DBG("%d %s no socket assigned", __LINE__, __func__); + LOG_DBG("%d no socket assigned", __LINE__); if (create_socket(sock, addr, socket_data.mdata_global) < 0) { return -1; } @@ -976,7 +1096,7 @@ static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t add false); if (ret < 0) { LOG_ERR("%s ret:%d", buf, ret); - errno = -ret; + errno = ret; return -1; } @@ -985,13 +1105,13 @@ static int offload_connect(void *obj, const struct sockaddr *addr, socklen_t add false); if (ret < 0) { LOG_ERR("%s ret:%d", buf, ret); - errno = -ret; + errno = ret; return -1; } ret = modem_cmd_send_int(socket_data.mdata_global, NULL, "", 0, &ktcpind_match, 1, false); if (ret) { LOG_ERR("Error sending data %d", ret); - ret = -ETIMEDOUT; + ret = ETIMEDOUT; LOG_ERR("%d No TCP_IND received, ret: %d", __LINE__, ret); return -1; } @@ -1018,7 +1138,8 @@ static int lock_socket_mutex(void) int ret = k_mutex_lock(&socket_data.mdata_global->tx_lock, K_SECONDS(1)); if (ret < 0) { - errno = -ret; + LOG_ERR("Failed to acquire TX lock: %d", ret); + errno = ret; } return ret; } @@ -1036,7 +1157,7 @@ static int wait_for_data_if_needed(struct modem_socket *sock, int flags) return -1; } - if (!sock->is_connected && sock->ip_proto != IPPROTO_UDP) { + if (validate_socket(sock) == -1) { errno = 0; return 0; } @@ -1068,13 +1189,14 @@ static void setup_socket_data(struct modem_socket *sock, struct socket_read_data socket_data.expected_buf_len = read_size + sizeof("\r\n") - 1 + socket_data.mdata_global->buffers.eof_pattern_size + sizeof(MODEM_STREAM_END_WORD) - 1; + socket_data.collected_buf_len = 0; + socket_data.socket_data_error = false; } static void restore_socket_state(void) { k_mutex_unlock(&socket_data.mdata_global->tx_lock); modem_chat_attach(&socket_data.mdata_global->chat, socket_data.mdata_global->uart_pipe); - k_work_submit(&socket_data.mdata_global->chat.receive_work); socket_data.expected_buf_len = 0; } @@ -1098,9 +1220,12 @@ static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, str struct socket_read_data sock_data; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif - + if (!hl78xx_is_registered(socket_data.mdata_global) || validate_socket(sock) == -1) { + errno = EAGAIN; + return -1; + } if (!validate_recv_args(buf, len, flags)) { return -1; } @@ -1127,7 +1252,7 @@ static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, str setup_socket_data(sock, &sock_data, buf, len, from, read_size); #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s socket_fd: %d, socket_id: %d, expected_data_len: %d", __LINE__, __func__, + LOG_DBG("%d socket_fd: %d, socket_id: %d, expected_data_len: %d", __LINE__, socket_data.current_sock_fd, socket_data.requested_socket_id, socket_data.expected_buf_len); LOG_HEXDUMP_DBG(sendbuf, strlen(sendbuf), "sending"); @@ -1156,28 +1281,13 @@ static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, str exit: #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d %d", __LINE__, __func__, errno, ret); + LOG_DBG("%d %d %d", __LINE__, errno, ret); #endif restore_socket_state(); check_tcp_state_if_needed(sock); return ret; } -static int validate_socket(const struct modem_socket *sock) -{ - - if (!sock) { - return -EINVAL; - } - - if (!sock->is_connected && sock->ip_proto != IPPROTO_UDP) { - errno = ENOTCONN; - return -1; - } - - return 0; -} - int check_if_any_socket_connected(void) { struct modem_socket_config *cfg = &socket_data.socket_config; @@ -1233,7 +1343,7 @@ static int send_data_buffer(const char *buf, const size_t buf_len, int *sock_wri int ret = 0; if (len == 0) { - LOG_DBG("%d %s No data to send", __LINE__, __func__); + LOG_DBG("%d No data to send", __LINE__); return 0; } @@ -1270,11 +1380,9 @@ static ssize_t send_socket_data(void *obj, const struct sockaddr *dst_addr, cons if (ret < 0) { return ret; } - if (!dst_addr && sock->ip_proto == IPPROTO_UDP) { dst_addr = &sock->dst; } - if (buf_len > MDM_MAX_DATA_LENGTH) { if (sock->type == SOCK_DGRAM) { errno = EMSGSIZE; @@ -1282,7 +1390,6 @@ static ssize_t send_socket_data(void *obj, const struct sockaddr *dst_addr, cons } buf_len = MDM_MAX_DATA_LENGTH; } - if (sock->ip_proto == IPPROTO_UDP) { ret = prepare_udp_send_cmd(sock, dst_addr, buf_len, cmd_buf, sizeof(cmd_buf)); } else { @@ -1292,18 +1399,22 @@ static ssize_t send_socket_data(void *obj, const struct sockaddr *dst_addr, cons return -1; } + socket_data.socket_data_error = false; + if (k_mutex_lock(&socket_data.mdata_global->tx_lock, K_SECONDS(1)) < 0) { - errno = -ret; + errno = ret; return -1; } - ret = modem_cmd_send_int(socket_data.mdata_global, NULL, cmd_buf, strlen(cmd_buf), connect_matches, ARRAY_SIZE(connect_matches), false); if (ret < 0) { LOG_ERR("Error sending AT command %d", ret); + } + if (socket_data.socket_data_error) { + ret = -ENODEV; + errno = ENODEV; goto cleanup; } - modem_pipe_attach(socket_data.mdata_global->chat.pipe, modem_pipe_callback, &socket_data.mdata_global->chat); @@ -1311,28 +1422,24 @@ static ssize_t send_socket_data(void *obj, const struct sockaddr *dst_addr, cons if (ret < 0) { goto cleanup; } - ret = k_sem_take(&socket_data.mdata_global->script_stopped_sem_tx_int, K_FOREVER); if (ret < 0) { goto cleanup; } - ret = modem_pipe_transmit(socket_data.mdata_global->uart_pipe, (uint8_t *)socket_data.mdata_global->buffers.eof_pattern, socket_data.mdata_global->buffers.eof_pattern_size); if (ret < 0) { LOG_ERR("Error sending EOF pattern: %d", ret); } - modem_chat_attach(&socket_data.mdata_global->chat, socket_data.mdata_global->uart_pipe); - ret = modem_cmd_send_int(socket_data.mdata_global, NULL, "", 0, &ok_match, 1, false); if (ret < 0) { LOG_ERR("Final confirmation failed: %d", ret); goto cleanup; } #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d %d", __LINE__, __func__, sock_written, ret); + LOG_DBG("%d %d %d", __LINE__, sock_written, ret); #endif cleanup: k_mutex_unlock(&socket_data.mdata_global->tx_lock); @@ -1346,12 +1453,25 @@ static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags, struct modem_socket *sock = (struct modem_socket *)obj; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d %d", __LINE__, len); #endif if (!hl78xx_is_registered(socket_data.mdata_global)) { LOG_ERR("Modem currently not attached to the network!"); - return -EAGAIN; + if (hl78xx_is_in_psm(socket_data.mdata_global) || + hl78xx_is_in_pwr_dwn(socket_data.mdata_global)) { + hl78xx_send_wakeup_signal(); + ret = k_sem_take(&socket_data.psm_cntrl_sem, K_SECONDS(55)); + if (ret == 0) { + LOG_DBG("Woke up modem from PSM"); + goto goon; + } else { + LOG_DBG("%d timeout waiting for modem wakeup", __LINE__); + } + } + errno = EAGAIN; + return -1; } +goon: /* Do some sanity checks. */ if (!buf || len == 0) { errno = EINVAL; @@ -1369,7 +1489,7 @@ static ssize_t offload_sendto(void *obj, const void *buf, size_t len, int flags, ret = send_socket_data(obj, to, buf, len, K_SECONDS(MDM_CMD_TIMEOUT)); if (ret < 0) { - errno = -ret; + errno = ret; return -1; } @@ -1382,7 +1502,7 @@ static int offload_ioctl(void *obj, unsigned int request, va_list args) int ret = 0; struct modem_socket *sock = (struct modem_socket *)obj; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s %d", __LINE__, __func__, request); + LOG_DBG("%d %d", __LINE__, request); #endif switch (request) { case ZFD_IOCTL_POLL_PREPARE: { @@ -1393,8 +1513,9 @@ static int offload_ioctl(void *obj, unsigned int request, va_list args) pfd = va_arg(args, struct zsock_pollfd *); pev = va_arg(args, struct k_poll_event **); pev_end = va_arg(args, struct k_poll_event *); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG LOG_DBG("poll_prepare: fd=%d, events=0x%x", pfd->fd, pfd->events); - +#endif ret = modem_socket_poll_prepare(&socket_data.socket_config, obj, pfd, pev, pev_end); if (ret == -1 && errno == ENOTSUP && (pfd->events & ZSOCK_POLLOUT) && @@ -1415,7 +1536,6 @@ static int offload_ioctl(void *obj, unsigned int request, va_list args) pfd = va_arg(args, struct zsock_pollfd *); pev = va_arg(args, struct k_poll_event **); - LOG_DBG("poll_update: fd=%d", pfd->fd); return modem_socket_poll_update(obj, pfd, pev); } @@ -1453,7 +1573,7 @@ static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags) int ret; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif /* Compute the full length to send and validate input */ for (int i = 0; i < msg->msg_iovlen; i++) { @@ -1503,7 +1623,7 @@ static ssize_t offload_sendmsg(void *obj, const struct msghdr *msg, int flags) } if (ret < 0) { - errno = -ret; + errno = ret; return -1; } @@ -1537,8 +1657,10 @@ static int hl78xx_init_sockets(const struct device *dev) socket_data.buf_pool = &mdm_recv_pool; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif + k_sem_init(&socket_data.psm_cntrl_sem, 0, 1); + /* socket config */ ret = modem_socket_init(&socket_data.socket_config, &socket_data.sockets[0], ARRAY_SIZE(socket_data.sockets), MDM_BASE_SOCKET_NUM, false, @@ -1551,7 +1673,7 @@ static int hl78xx_init_sockets(const struct device *dev) return ret; } -void socknotifydata(int socket_id, int new_total) +void socket_notify_data(int socket_id, int new_total) { struct modem_socket *sock; int ret = 0; @@ -1570,13 +1692,80 @@ void socknotifydata(int socket_id, int new_total) } } +void tcp_notify_data(int socket_id, int tcp_notif) +{ + enum hl78xx_tcp_notif tcp_notif_received = tcp_notif; + + socket_data.requested_socket_id = socket_id; + + switch (tcp_notif_received) { + case TCP_NOTIF_REMOTE_DISCONNECTION: + socket_notify_data(socket_id, 1); + break; + case TCP_NOTIF_NETWORK_ERROR: + /* Handle network error */ + break; + default: + break; + } +} + +static void l4_event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, + struct net_if *iface) +{ + struct hl78xx_socket_data *data = CONTAINER_OF(cb, struct hl78xx_socket_data, l4_cb); + + ARG_UNUSED(data); + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_DBG("%d mgmt_event: %lld", __LINE__, mgmt_event); +#endif + + switch (mgmt_event) { + case NET_EVENT_DNS_SERVER_ADD: + /* Do something */ + LOG_INF("Network connectivity established and IP address assigned IPv4: %s IPv6: " + "%s", + data->dns_v4_string, data->dns_v6_string); + break; + case NET_EVENT_L4_CONNECTED: + /* Do something */ + break; + case NET_EVENT_L4_DISCONNECTED: + /* Do something */ + break; + default: + break; + } +} +static void connectivity_event_handler(struct net_mgmt_event_callback *cb, uint64_t event, + struct net_if *iface) +{ + if (event == CONN_LAYER_EVENT_MASK) { + LOG_ERR("Fatal error received from the connectivity layer"); + /* Do something */ + return; + } +} + void hl78xx_socket_init(struct hl78xx_data *data) { #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif socket_data.mdata_global = data; atomic_set(&state_leftover, 0); + + { + /* Setup handler for Zephyr NET Connection Manager events. */ + net_mgmt_init_event_callback(&socket_data.l4_cb, l4_event_handler, L4_EVENT_MASK); + net_mgmt_add_event_callback(&socket_data.l4_cb); + + /* Setup handler for Zephyr NET Connection Manager Connectivity layer. */ + net_mgmt_init_event_callback(&socket_data.conn_cb, connectivity_event_handler, + CONN_LAYER_EVENT_MASK); + net_mgmt_add_event_callback(&socket_data.conn_cb); + } } static void modem_net_iface_init(struct net_if *iface) @@ -1584,7 +1773,7 @@ static void modem_net_iface_init(struct net_if *iface) const struct device *dev = net_if_get_device(iface); struct hl78xx_socket_data *data = dev->data; #ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG - LOG_DBG("%d %s", __LINE__, __func__); + LOG_DBG("%d", __LINE__); #endif net_if_set_link_addr( iface, modem_get_mac(socket_data.mac_addr, socket_data.mdata_global->identity.imei), diff --git a/drivers/modem/hl78xx/hl78xx_utility.c b/drivers/modem/hl78xx/hl78xx_utility.c index b6c749274275..7a9b9b0178d3 100644 --- a/drivers/modem/hl78xx/hl78xx_utility.c +++ b/drivers/modem/hl78xx/hl78xx_utility.c @@ -50,11 +50,41 @@ int modem_atoi(const char *s, const int err_value, const char *desc, const char return ret; } +double modem_atod(const char *s, const double err_value, const char *desc, const char *func) +{ + double ret; + char *endptr; + ret = strtod(s, &endptr); + if (!endptr || *endptr != '\0') { + LOG_ERR("bad %s '%s' in %s", s, desc, func); + return err_value; + } + + return ret; +} bool hl78xx_is_registered(struct hl78xx_data *data) { - return (data->status.registration.network_state == HL78XX_REGISTRATION_REGISTERED_HOME) || - (data->status.registration.network_state == HL78XX_REGISTRATION_REGISTERED_ROAMING); + return (data->status.registration.network_state_current == + HL78XX_REGISTRATION_REGISTERED_HOME) || + (data->status.registration.network_state_current == + HL78XX_REGISTRATION_REGISTERED_ROAMING); +} + +bool hl78xx_is_rsrp_valid(struct hl78xx_data *data) +{ + return (data->status.rsrp >= CONFIG_MODEM_MIN_ALLOWED_SIGNAL_STRENGTH); +} + +bool hl78xx_is_in_psm(struct hl78xx_data *data) +{ + return (data->status.psm.psmev_current == HL78XX_PSM_EVENT_ENTER); +} + +bool hl78xx_is_in_pwr_dwn(struct hl78xx_data *data) +{ + return (data->status.pmc_power_down.requested_currently == true && + data->status.pmc_power_down.status_currently == true); } #define HASH_MULTIPLIER 37 @@ -489,3 +519,38 @@ void hl78xx_extract_essential_part_apn(const char *full_apn, char *essential_apn essential_apn[max_len - 1] = '\0'; } } + +/* Convert 8-character binary string to byte (0–255) */ +int binary_str_to_byte(const char *bin_str) +{ + if (strlen(bin_str) != 8) { + return -EINVAL; /* Invalid input length */ + } + + int value = 0; + + for (int i = 0; i < 8; i++) { + if (bin_str[i] == '1') { + value = (value << 1) | 1; + } else if (bin_str[i] == '0') { + value = value << 1; + } else { + return -EINVAL; /* Invalid character */ + } + } + + return value; +} + +/* Convert byte to 8-character binary string */ +void byte_to_binary_str(uint8_t byte, char *output) +{ + if (output == NULL) { + return; /* Invalid output pointer */ + } + + for (int i = 7; i >= 0; i--) { + output[7 - i] = (byte & (1 << i)) ? '1' : '0'; + } + output[8] = '\0'; +} diff --git a/include/zephyr/drivers/modem/hl78xx_apis.h b/include/zephyr/drivers/modem/hl78xx_apis.h index 083b1f84b1d4..202471bdbec0 100644 --- a/include/zephyr/drivers/modem/hl78xx_apis.h +++ b/include/zephyr/drivers/modem/hl78xx_apis.h @@ -114,11 +114,18 @@ enum hl78xx_registration_status { HL78XX_REGISTRATION_REGISTERED_ROAMING, }; +enum hl78xx_psmev_event { + HL78XX_PSM_EVENT_EXIT = 0, + HL78XX_PSM_EVENT_ENTER, + HL78XX_PSM_EVENT_NONE, +}; + enum hl78xx_evt_type { HL78XX_RAT_UPDATE, HL78XX_LTE_REGISTRATION_STAT_UPDATE, HL78XX_LTE_SIM_REGISTRATION, HL78XX_LTE_PSMEV, + HL78XX_LTE_MODEM_STARTUP, }; struct hl78xx_evt { @@ -127,6 +134,10 @@ struct hl78xx_evt { union { enum hl78xx_registration_status reg_status; enum hl78xx_cell_rat_mode rat_mode; + enum hl78xx_psmev_event psm_event; + bool status; + int value; + } content; }; /** API for configuring networks */ @@ -465,7 +476,7 @@ static inline int hl78xx_parse_rssi(uint8_t rssi, int16_t *value) * and converts it into a corresponding signal strength in dBm, typically based on * 3GPP TS 36.133 specifications. * - * @param rsrp Raw RSRP value (commonly in the range 0–97, or 255 if unknown). + * @param rsrp Raw RSRP value (commonly in the range = -140.0 dBm to 0.0 dBm, or 255 if unknown). * @param value Pointer to store the converted RSRP in dBm. * * @retval 0 on successful conversion. From 09a50d1c3c79cfdc8b1dd99a587e1729063164e5 Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Mon, 30 Jun 2025 22:08:08 +0100 Subject: [PATCH 7/7] samples: drivers: modem: hello_hl78xx sample Low Power Add low power config files Signed-off-by: Zafer SEN --- .../overlay-swir_hl78xx-low-power.conf | 21 ++++++ .../overlay-swir_hl78xx-verbose-logging.conf | 10 +++ .../overlay-swir_hl78xx_ev_kit.conf | 71 +++++++++++++++++++ samples/drivers/modem/hello_hl78xx/prj.conf | 5 -- 4 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-low-power.conf create mode 100644 samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-verbose-logging.conf create mode 100644 samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx_ev_kit.conf diff --git a/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-low-power.conf b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-low-power.conf new file mode 100644 index 000000000000..f1ada5a528e4 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-low-power.conf @@ -0,0 +1,21 @@ +# Sierra Wireless HL78XX driver driver options + +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +# The HL78xx driver gets its IP settings from the cell network + +#PM +CONFIG_PM_DEVICE=y + +#Modem low power configuration +CONFIG_MODEM_HL78XX_LOW_POWER_MODE=y +#enable PSM +CONFIG_MODEM_HL78XX_PSM=y +#enable eDRX +CONFIG_MODEM_HL78XX_EDRX=n +#enable power down mode +#default: MODEM_HL78XX_USE_DELAY_BASED_POWER_DOWN +# CONFIG_MODEM_HL78XX_POWER_DOWN=y +#enable active time based power down +# CONFIG_MODEM_HL78XX_USE_ACTIVE_TIME_BASED_POWER_DOWN=y diff --git a/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-verbose-logging.conf b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-verbose-logging.conf new file mode 100644 index 000000000000..ebd3a7e9967a --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-verbose-logging.conf @@ -0,0 +1,10 @@ +# Logging +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BUFFER_SIZE=85536 +CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=8192 + +# For extra verbosity +CONFIG_MODEM_MODULES_LOG_LEVEL_DBG=y +CONFIG_MODEM_LOG_LEVEL_DBG=y +CONFIG_MODEM_CHAT_LOG_BUFFER_SIZE=1024 +CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG=y diff --git a/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx_ev_kit.conf b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx_ev_kit.conf new file mode 100644 index 000000000000..156088d8dd67 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx_ev_kit.conf @@ -0,0 +1,71 @@ +# Sierra Wireless HL78XX driver options + +# Copyright (c) 2025 Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +# The HL78xx driver gets its IP settings from the cell network +CONFIG_NET_CONFIG_SETTINGS=n +CONFIG_NET_DHCPV4=n +CONFIG_DNS_SERVER_IP_ADDRESSES=n + +#uart +CONFIG_UART_ASYNC_API=y +CONFIG_UART_INTERRUPT_DRIVEN=n + +# Generic networking options +CONFIG_NET_IPV6=n + +# SNTP +CONFIG_NET_CONFIG_SNTP_INIT_SERVER="time.google.com" + +# DNS +CONFIG_NET_SOCKETS_DNS_TIMEOUT=15000 + +# Wait for the network to be ready +CONFIG_NET_SAMPLE_COMMON_WAIT_DNS_SERVER_ADDITION=y + +# Network management +CONFIG_NET_MGMT=y +CONFIG_NET_MGMT_EVENT=y +CONFIG_NET_CONNECTION_MANAGER=y + +# NB-IoT has large latency, so increase timeouts. It is ok to use this for Cat-M1 as well. +CONFIG_NET_SOCKETS_CONNECT_TIMEOUT=15000 + +# Network buffers +CONFIG_NET_PKT_RX_COUNT=32 +CONFIG_NET_PKT_TX_COUNT=16 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=32 + +# Modem driver +CONFIG_MODEM=y + +#hl78xx modem +CONFIG_MODEM_HL78XX=y +CONFIG_MODEM_HL7812=y +CONFIG_MODEM_FW_R6=y + +# CONFIG_MODEM_HL78XX_RX_WORKQ_STACK_SIZE=4096 + +# Statistics +CONFIG_MODEM_STATS=y +CONFIG_SHELL=y +# Don't require device to have time/date +CONFIG_MBEDTLS_HAVE_TIME_DATE=n + +#apn source +# CONFIG_MODEM_HL78XX_APN_SOURCE_KCONFIG=y +# CONFIG_MODEM_HL78XX_APN="internet" + +CONFIG_MODEM_HL78XX_BOOT_IN_FULLY_FUNCTIONAL_MODE=y + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y + +CONFIG_MODEM_BACKEND_UART_ASYNC=y +CONFIG_MODEM_BACKEND_UART_ISR=n diff --git a/samples/drivers/modem/hello_hl78xx/prj.conf b/samples/drivers/modem/hello_hl78xx/prj.conf index e1325db0bcbb..2f2b0ea5f504 100644 --- a/samples/drivers/modem/hello_hl78xx/prj.conf +++ b/samples/drivers/modem/hello_hl78xx/prj.conf @@ -74,8 +74,3 @@ CONFIG_HL78XX_EVT_MONITOR=y CONFIG_LOG=y CONFIG_LOG_MODE_DEFERRED=y CONFIG_LOG_BUFFER_SIZE=32768 -# For extra verbosity -# CONFIG_MODEM_MODULES_LOG_LEVEL_DBG=y -# CONFIG_MODEM_LOG_LEVEL_DBG=y -# CONFIG_MODEM_CHAT_LOG_BUFFER_SIZE=1024 -# CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG=y