From a2865af00d2b0d09c93024ea5c7d91065838206e 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 | 28 + drivers/modem/hl78xx/Kconfig.hl78xx | 822 ++++++ drivers/modem/hl78xx/hl78xx.c | 1860 ++++++++++++ drivers/modem/hl78xx/hl78xx.h | 652 +++++ drivers/modem/hl78xx/hl78xx_apis.c | 291 ++ drivers/modem/hl78xx/hl78xx_cfg.c | 587 ++++ drivers/modem/hl78xx/hl78xx_cfg.h | 61 + drivers/modem/hl78xx/hl78xx_chat.c | 376 +++ drivers/modem/hl78xx/hl78xx_chat.h | 69 + .../hl78xx/hl78xx_evt_monitor/CMakeLists.txt | 10 + .../Kconfig.hl78xx_evt_monitor | 29 + .../hl78xx_evt_monitor/hl78xx_evt_monitor.c | 172 ++ .../hl78xx_evt_monitor/hl78xx_evt_monitor.ld | 11 + drivers/modem/hl78xx/hl78xx_sockets.c | 2608 +++++++++++++++++ include/zephyr/drivers/modem/hl78xx_apis.h | 458 +++ 17 files changed, 8037 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_cfg.c create mode 100644 drivers/modem/hl78xx/hl78xx_cfg.h create mode 100644 drivers/modem/hl78xx/hl78xx_chat.c create mode 100644 drivers/modem/hl78xx/hl78xx_chat.h 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 include/zephyr/drivers/modem/hl78xx_apis.h diff --git a/drivers/modem/CMakeLists.txt b/drivers/modem/CMakeLists.txt index adc6614dc7fe5..4891e11cc333c 100644 --- a/drivers/modem/CMakeLists.txt +++ b/drivers/modem/CMakeLists.txt @@ -35,6 +35,8 @@ 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_USER_PIPE modem_at_user_pipe.c) zephyr_library_sources_ifdef(CONFIG_MODEM_AT_SHELL modem_at_shell.c) diff --git a/drivers/modem/Kconfig b/drivers/modem/Kconfig index b8e66ef427482..833bebff31784 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 0000000000000..1320925476238 --- /dev/null +++ b/drivers/modem/hl78xx/CMakeLists.txt @@ -0,0 +1,28 @@ +# +# Copyright (c) 2025 Netfeasa Ltd. +# +# SPDX-License-Identifier: Apache-2.0 +# +zephyr_library() + +zephyr_library_sources( + hl78xx.c + hl78xx_sockets.c + hl78xx_cfg.c + hl78xx_chat.c + hl78xx_apis.c +) + +add_subdirectory_ifdef(CONFIG_HL78XX_EVT_MONITOR hl78xx_evt_monitor) + +zephyr_library_include_directories( + ./ + # IP headers + ${ZEPHYR_BASE}/subsys/net/ip + ${ZEPHYR_BASE}/subsys/net/lib/sockets +) + +zephyr_library_include_directories_ifdef( + CONFIG_NET_SOCKETS_SOCKOPT_TLS + ${ZEPHYR_BASE}/subsys/net/lib/tls_credentials +) diff --git a/drivers/modem/hl78xx/Kconfig.hl78xx b/drivers/modem/hl78xx/Kconfig.hl78xx new file mode 100644 index 0000000000000..bed24a07276fa --- /dev/null +++ b/drivers/modem/hl78xx/Kconfig.hl78xx @@ -0,0 +1,822 @@ +# 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_HL78XX_12 if DT_HAS_SWIR_HL7812_ENABLED + default MODEM_HL78XX_00 if DT_HAS_SWIR_HL7800_ENABLED + default MODEM_HL78XX_AUTODETECT_VARIANT + +config MODEM_HL78XX_12 + bool "Sierra Wireless hl7812" + help + Support for hl7812 modem + +config MODEM_HL78XX_00 + bool "Sierra Wireless hl7800" + help + Support for hl7800 modem + +config MODEM_HL78XX_AUTODETECT_VARIANT + bool "detect automatically" + help + Automatic detection of modem variant (HL7812 or HL7800) + +endchoice + +if MODEM_HL78XX_12 + +config MODEM_HL78XX_12_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_HL78XX_12 + +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_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_FULLY_FUNCTIONAL_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_HL78XX_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" + 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" + default "1,2,3" if MODEM_HL78XX_12 + default "1,2,1" if MODEM_HL78XX_00 + help + AT+KSELACQ=0,1,2,3 , MODEM HL7812,CAT-M1, NB-IoT, GSM + AT+KSELACQ=0,1,2,1 , MODEM HL7800,CAT-M1, NB-IoT, CAT-M1 + +config MODEM_HL78XX_AUTORAT_NB_BAND_CFG + string "NB-IoT band configuration (comma-separated list)" + default "1,2,3,4,5,8,12,13,20,28" + 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 "1,2,3,4,5,8,12,13,20,28" + 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_HL78XX_12 + help + Enable GSM mode during modem init. + 2 — GSM (for HL7802/HL7812 only) + +config MODEM_HL78XX_RAT_NBNTN + bool "NB-NTN" + depends on MODEM_HL78XX_12_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" + depends on !MODEM_HL78XX_AUTORAT + 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 +if !MODEM_HL78XX_RAT_NBNTN +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) +endif # !MODEM_HL78XX_RAT_NBNTN +config MODEM_HL78XX_BAND_23 + bool "Band 23 (2000MHz)" + default y if MODEM_HL78XX_RAT_NBNTN + help + Enable Band 23 (2000MHz) +if !MODEM_HL78XX_RAT_NBNTN +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) +endif # !MODEM_HL78XX_RAT_NBNTN +config MODEM_HL78XX_BAND_255 + bool "Band 255 (1500MHz)" + default y if MODEM_HL78XX_RAT_NBNTN + help + Enable Band 255 (1500MHz) + +config MODEM_HL78XX_BAND_256 + bool "Band 256 (2000MHz)" + default y if MODEM_HL78XX_RAT_NBNTN + help + Enable Band 256 (2000MHz) + +endif # MODEM_HL78XX_CONFIGURE_BAND + +# NB-IoT NTN Position Configuration +if MODEM_HL78XX_RAT_NBNTN +menuconfig MODEM_HL78XX_NBNTN_POSITIONING + bool "NB-IoT NTN Position Configuration" + depends on !MODEM_HL78XX_AUTORAT + default y if !MODEM_HL78XX_AUTORAT + help + Choose this setting to configure NB-IoT NTN Positioning parameters. + Only applicable if NB-NTN mode is selected (MODEM_HL78XX_RAT_NBNTN). + +if MODEM_HL78XX_NBNTN_POSITIONING + +choice + prompt "Position Source" + default NTN_POSITION_SOURCE_IGNSS + help + Select the source of UE (User Equipment) position for NTN TA calculation. + IGNSS: Use HL781x internal GNSS to acquire position automatically. + MANUAL: Manually enter UE position using +KNTNCMD AT command. + +config NTN_POSITION_SOURCE_IGNSS + bool "IGNSS (internal GNSS)" + help + Use HL781x internal GNSS to acquire position automatically. + +config NTN_POSITION_SOURCE_MANUAL + bool "MANUAL (manual entry)" + help + Manually enter UE position using +KNTNCMD AT command. + +endchoice + +choice + prompt "Mobility Type" + default NTN_MOBILITY_TYPE_STATIC + help + Specify the UE's mobility type. + STATIC: Position remains fixed (within 600 meters of initial fix). + DYNAMIC: Position may change, requires update for each network operation. + +config NTN_MOBILITY_TYPE_STATIC + bool "STATIC (fixed position)" + help + Position remains fixed (within 600 meters of initial fix). + +config NTN_MOBILITY_TYPE_DYNAMIC + bool "DYNAMIC (moving position)" + help + Position may change, requires update for each network operation. + +endchoice + +config NTN_STATIC_THRESHOLD + int "Static Mode Threshold (meters)" + default 600 + help + Distance between current position and initial fix that determines static/dynamic mode. + If position variation exceeds this threshold, dynamic mode is recommended. + For documentation/reference only. + +config NTN_DYNAMIC_POSREQ_UPDATE + bool "Require position update on POSREQ in Dynamic Mode" + default y + help + If enabled, the UE position must be updated after receiving unsolicited +KNTNEV: + 'POSREQ' indication, before any outgoing network operation in dynamic mode. +endif # MODEM_HL78XX_NBNTN_POSITIONING +endif # MODEM_HL78XX_RAT_NBNTN + +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" + help + Enable LTE eDRX + +config MODEM_HL78XX_PSM + bool "PSM" + 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 + +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 + Enable advanced socket configuration options + +if MODEM_HL78XX_ADVANCED_SOCKET_CONFIG + +config MODEM_HL78XX_SOCKET_UDP_DISPLAY_DATA_URC + int "display data in URC" + default 0 + 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_NUM_SOCKETS + int "Maximum number of sockets" + default 6 + range 6 6 if !MODEM_HL78XX_ADVANCED_SOCKET_CONFIG + range 1 6 if MODEM_HL78XX_ADVANCED_SOCKET_CONFIG + help + Maximum number of sockets that can be opened at the same time + +config MODEM_HL78XX_SOCKETS_SOCKOPT_TLS + bool "TLS for sockets" + depends on NET_SOCKETS_SOCKOPT_TLS + help + This option enables TLS (Transport Layer Security) for sockets + on the HL78xx modem. + +config MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + bool "Verbose debug output in the HL78xx" + depends on MODEM_MODULES_LOG_LEVEL_DBG + help + Enabling this setting will turn on VERY heavy debugging from the + modem. Do NOT leave on for production. + This setting is depends on global log level debug. + +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 + 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 81 + 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 0000000000000..92542a447abf2 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx.c @@ -0,0 +1,1860 @@ +/* + * 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" +#include "hl78xx_chat.h" +#include "hl78xx_cfg.h" + +#define MAX_SCRIPT_AT_CMD_RETRY 3 + +#define MDM_NODE DT_ALIAS(modem) +/* Check phandle target status for a specific phandle index */ +#define HAS_GPIO_IDX(node_id, prop, idx) DT_PROP_HAS_IDX(node_id, prop, idx) + +/* GPIO availability macros */ +#define HAS_RESET_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_reset_gpios, 0) +#define HAS_WAKE_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_wake_gpios, 0) +#define HAS_VGPIO_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_vgpio_gpios, 0) +#define HAS_UART_CTS_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_uart_cts_gpios, 0) +#define HAS_GPIO6_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_gpio6_gpios, 0) +#define HAS_PWR_ON_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_pwr_on_gpios, 0) +#define HAS_FAST_SHUTD_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_fast_shutd_gpios, 0) +#define HAS_UART_DSR_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_uart_dsr_gpios, 0) +#define HAS_UART_DTR_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_uart_dtr_gpios, 0) +#define HAS_GPIO8_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_gpio8_gpios, 0) +#define HAS_SIM_SWITCH_GPIO HAS_GPIO_IDX(MDM_NODE, mdm_sim_switch_gpios, 0) + +/* GPIO count macro */ +#define GPIO_CONFIG_LEN \ + (HAS_RESET_GPIO + HAS_WAKE_GPIO + HAS_VGPIO_GPIO + HAS_UART_CTS_GPIO + HAS_GPIO6_GPIO + \ + HAS_PWR_ON_GPIO + HAS_FAST_SHUTD_GPIO + HAS_UART_DSR_GPIO + HAS_UART_DTR_GPIO + \ + HAS_GPIO8_GPIO + HAS_SIM_SWITCH_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_dispatcher_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); + +struct hl78xx_state_handlers { + int (*on_enter)(struct hl78xx_data *data); + int (*on_leave)(struct hl78xx_data *data); + void (*on_event)(struct hl78xx_data *data, enum hl78xx_event evt); +}; + +/* Forward declare the table so functions earlier in this file can reference + * it. The table itself is defined later in the file (without 'static'). + */ +const static struct hl78xx_state_handlers hl78xx_state_table[]; +/** Dispatch an event to the registered event dispatcher, if any. + * + */ +static void event_dispatcher_dispatch(struct hl78xx_evt *notif) +{ + if (event_dispatcher != NULL) { + event_dispatcher(notif); + } +} +/* ------------------------------------------------------------------------- + * Utilities + * - small helpers and local utility functions + * ------------------------------------------------------------------------- + */ + +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_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_REGISTERED: + return "await registered"; + case MODEM_HL78XX_STATE_CARRIER_ON: + return "carrier on"; + case MODEM_HL78XX_STATE_CARRIER_OFF: + return "carrier off"; + case MODEM_HL78XX_STATE_SIM_POWER_OFF: + return "sim power off"; + case MODEM_HL78XX_STATE_AIRPLANE: + return "airplane mode"; + 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_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_INF("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); + LOG_DBG("dequeued %d events", events_cnt); + + 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); +} +/* ------------------------------------------------------------------------- + * Chat callbacks / URC handlers + * - unsolicited response handlers and chat-related parsers + * ------------------------------------------------------------------------- + */ +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 cellular_registration_status registration_status = 0; + struct hl78xx_evt event = {.type = HL78XX_LTE_REGISTRATION_STAT_UPDATE}; +#ifndef CONFIG_MODEM_HL78XX_12 + enum hl78xx_cell_rat_mode rat_mode = HL78XX_RAT_MODE_NONE; + struct hl78xx_evt rat_event; + bool rat_mode_updated = false; + int act_value = -1; +#endif /* CONFIG_MODEM_HL78XX_12 */ + if (argc < 2) { + return; + } + /* +CXREG: [,[...]] */ + 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], 0, "registration_status"); +#ifndef CONFIG_MODEM_HL78XX_12 + if (argc > 4 && strlen(argv[5]) == 1) { + act_value = ATOI(argv[5], -1, "act_value"); + LOG_DBG("act_value: %d, argc: %d, argv[5]: %s", act_value, argc, argv[5]); + switch (act_value) { + case 7: + rat_mode = HL78XX_RAT_CAT_M1; + break; + case 9: + rat_mode = HL78XX_RAT_NB1; + break; + default: + rat_mode = HL78XX_RAT_MODE_NONE; + break; + } + rat_mode_updated = true; + LOG_DBG("RAT mode from response: %d", rat_mode); + } +#endif /* CONFIG_MODEM_HL78XX_12 */ + } else { + registration_status = ATOI(argv[1], 0, "registration_status"); +#ifndef CONFIG_MODEM_HL78XX_12 + if (argc > 3 && strlen(argv[4]) == 1) { + act_value = ATOI(argv[4], -1, "act_value"); + LOG_DBG("act_value: %d, argc: %d, argv[4]: %s", act_value, argc, argv[4]); + switch (act_value) { + case 7: + rat_mode = HL78XX_RAT_CAT_M1; + break; + case 9: + rat_mode = HL78XX_RAT_NB1; + break; + default: + rat_mode = HL78XX_RAT_MODE_NONE; + break; + } + rat_mode_updated = true; + LOG_DBG("RAT mode from URC: %d", rat_mode); + } +#endif /* CONFIG_MODEM_HL78XX_12 */ + } + HL78XX_LOG_DBG("%s: %d", argv[0], registration_status); + if (registration_status == data->status.registration.network_state_current) { +#ifndef CONFIG_MODEM_HL78XX_12 + /* Check if RAT mode changed even if registration status didn't */ + if (rat_mode_updated && rat_mode != -1 && + rat_mode != data->status.registration.rat_mode) { + data->status.registration.rat_mode = rat_mode; + rat_event.type = HL78XX_LTE_RAT_UPDATE; + rat_event.content.rat_mode = rat_mode; + event_dispatcher_dispatch(&rat_event); + } +#endif /* CONFIG_MODEM_HL78XX_12 */ + return; + } + /* Update the previous registration state */ + 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; + event.content.reg_status = data->status.registration.network_state_current; + + data->status.registration.is_registered_previously = + data->status.registration.is_registered_currently; +#ifndef CONFIG_MODEM_HL78XX_12 + /* Update RAT mode if parsed */ + if (rat_mode_updated && rat_mode != -1 && rat_mode != data->status.registration.rat_mode) { + data->status.registration.rat_mode = rat_mode; + rat_event.type = HL78XX_LTE_RAT_UPDATE; + rat_event.content.rat_mode = rat_mode; + event_dispatcher_dispatch(&rat_event); + } +#endif /* CONFIG_MODEM_HL78XX_12 */ + /* Update current registration flag */ + if (hl78xx_is_registered(data)) { + data->status.registration.is_registered_currently = true; + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_REGISTERED); +#ifdef CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING + k_sem_give(&data->stay_in_boot_mode_sem); +#endif /* CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING */ + } else { + data->status.registration.is_registered_currently = false; + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_DEREGISTERED); + } + event_dispatcher_dispatch(&event); +} + +void hl78xx_on_ksup(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + int module_status; + struct hl78xx_evt event = {.type = HL78XX_LTE_MODEM_STARTUP}; + + if (argc != 2) { + return; + } + module_status = ATOI(argv[1], 0, "module_status"); + event.content.value = module_status; + event_dispatcher_dispatch(&event); + HL78XX_LOG_DBG("Module status: %d", module_status); +} + +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; + } + HL78XX_LOG_DBG("IMEI: %s %s", argv[0], argv[1]); + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy((char *)data->identity.imei, argv[1], sizeof(data->identity.imei)); + k_mutex_unlock(&data->api_lock); +} + +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; + } + HL78XX_LOG_DBG("cgmm: %s %s", argv[0], argv[1]); + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy((char *)data->identity.model_id, argv[1], sizeof(data->identity.model_id)); + k_mutex_unlock(&data->api_lock); +} + +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; + } + HL78XX_LOG_DBG("IMSI: %s %s", argv[0], argv[1]); + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy((char *)data->identity.imsi, argv[1], sizeof(data->identity.imsi)); + k_mutex_unlock(&data->api_lock); +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI) + /* set the APN automatically */ + modem_detect_apn(data, argv[1]); +#endif /* CONFIG_MODEM_HL78XX_APN_SOURCE_IMSI */ +} + +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; + } + HL78XX_LOG_DBG("cgmi: %s %s", argv[0], argv[1]); + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy((char *)data->identity.manufacturer, argv[1], + sizeof(data->identity.manufacturer)); + k_mutex_unlock(&data->api_lock); +} + +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; + } + HL78XX_LOG_DBG("cgmr: %s %s", argv[0], argv[1]); + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy((char *)data->identity.fw_version, argv[1], sizeof(data->identity.fw_version)); + k_mutex_unlock(&data->api_lock); +} + +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; + } + HL78XX_LOG_DBG("ICCID: %s %s", argv[0], argv[1]); + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy((char *)data->identity.iccid, argv[1], sizeof(data->identity.iccid)); + k_mutex_unlock(&data->api_lock); + +#if defined(CONFIG_MODEM_HL78XX_APN_SOURCE_ICCID) + /* set the APN automatically */ + modem_detect_apn(data, argv[1]); +#endif /* CONFIG_MODEM_HL78XX_APN_SOURCE_ICCID */ +} + +#if defined(CONFIG_MODEM_HL78XX_12) +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; + struct hl78xx_evt event = {.type = HL78XX_LTE_RAT_UPDATE}; + + if (argc != 3) { + return; + } + rat_mode = ATOI(argv[2], 0, "rat_mode"); +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + hl78xx_on_kstatev_parser(data, (enum hl78xx_info_transfer_event)ATOI(argv[1], 0, "status"), + rat_mode); +#endif /* CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG */ + if (rat_mode != data->status.registration.rat_mode) { + data->status.registration.rat_mode = rat_mode; + event.content.rat_mode = data->status.registration.rat_mode; + event_dispatcher_dispatch(&event); + } +} +#endif + +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 = ATOI(argv[1], 0, "ksrep"); + HL78XX_LOG_DBG("KSREP: %s %s", argv[0], argv[1]); +} +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; + struct hl78xx_evt event = {.type = HL78XX_LTE_RAT_UPDATE}; + + if (argc < 2) { + return; + } + data->status.registration.rat_mode = (uint8_t)ATOI(argv[1], 0, "rat_mode"); + event.content.rat_mode = data->status.registration.rat_mode; + event_dispatcher_dispatch(&event); + HL78XX_LOG_DBG("KSRAT: %s %s", argv[0], argv[1]); +} + +void hl78xx_on_kselacq(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; + } + 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; + } +} + +void hl78xx_on_kbndcfg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + uint8_t rat_id; + uint8_t kbnd_bitmap_size; + + if (argc < 3) { + return; + } + + rat_id = ATOI(argv[1], 0, "rat"); + kbnd_bitmap_size = strlen(argv[2]); + HL78XX_LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], 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'; +} + +void hl78xx_on_csq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc < 3) { + return; + } + data->status.rssi = ATOI(argv[1], 0, "rssi"); +} + +void hl78xx_on_cesq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc < 7) { + return; + } + data->status.rsrq = ATOI(argv[5], 0, "rsrq"); + data->status.rsrp = ATOI(argv[6], 0, "rsrp"); +} + +void hl78xx_on_cfun(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.phone_functionality.functionality = ATOI(argv[1], 0, "phone_func"); + data->status.phone_functionality.in_progress = false; +} + +void hl78xx_on_cops(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + + if (argc < 3) { + return; + } + safe_strncpy((char *)data->status.network_operator.operator, argv[3], + sizeof(data->status.network_operator.operator)); + data->status.network_operator.format = ATOI(argv[2], 0, "network_operator_format"); +} + +/* ------------------------------------------------------------------------- + * Pipe & chat initialization + * - modem backend pipe setup and chat initialisation helpers + * ------------------------------------------------------------------------- + */ +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); +} + +/* Initialize the modem chat subsystem using wrappers from hl78xx_chat.c */ +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 = (uint16_t)ARRAY_SIZE(data->buffers.argv), + .unsol_matches = hl78xx_get_unsol_matches(), + .unsol_matches_size = (uint16_t)hl78xx_get_unsol_matches_size(), + }; + + return modem_chat_init(&data->chat, &chat_config); +} + +/* clang-format off */ +int modem_dynamic_cmd_send( + 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; + int script_ret = 0; + /* validate input parameters */ + if (data == NULL) { + LOG_ERR("%d %s Invalid parameter", __LINE__, __func__); + errno = EINVAL; + return -1; + } + 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 = hl78xx_get_abort_matches(), + .abort_matches_size = hl78xx_get_abort_matches_size(), + .callback = script_user_callback, + .timeout = 1000 + }; + + ret = k_mutex_lock(&data->tx_lock, K_NO_WAIT); + if (ret < 0) { + if (user_cmd == false) { + errno = -ret; + } + return -1; + } + /* run the chat script */ + script_ret = modem_chat_run_script(&data->chat, &chat_script); + if (script_ret < 0) { + LOG_ERR("%d %s Failed to run at command: %d", __LINE__, __func__, script_ret); + } else { + LOG_DBG("Chat script executed successfully."); + } + ret = k_mutex_unlock(&data->tx_lock); + if (ret < 0) { + if (user_cmd == false) { + errno = -ret; + } + /* we still return the script result if available, prioritize script_ret */ + return script_ret < 0 ? -1 : script_ret; + } + return script_ret; +} +/* clang-format on */ + +/* ------------------------------------------------------------------------- + * GPIO ISR callbacks + * - lightweight wrappers for GPIO interrupts (logging & event dispatch) + * ------------------------------------------------------------------------- + */ +void mdm_vgpio_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t 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; + } + if (!(pins & BIT(spec->pin))) { + return; /* not our pin */ + } + LOG_DBG("VGPIO ISR callback %s %d %d", spec->port->name, spec->pin, gpio_pin_get_dt(spec)); +} + +#if HAS_UART_DSR_GPIO +void mdm_uart_dsr_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t 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; + + 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)); +} +#endif + +void mdm_gpio6_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t 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; + + 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 */ + } + LOG_DBG("GPIO6 ISR callback %s %d %d", spec->port->name, spec->pin, gpio_pin_get_dt(spec)); +} + +void mdm_uart_cts_callback_isr(const struct device *port, struct gpio_callback *cb, uint32_t 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; + + 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)); +} + +bool hl78xx_is_registered(struct hl78xx_data *data) +{ + return (data->status.registration.network_state_current == + CELLULAR_REGISTRATION_REGISTERED_HOME) || + (data->status.registration.network_state_current == + CELLULAR_REGISTRATION_REGISTERED_ROAMING); +} + +/* + * hl78xx_is_registered - convenience helper + * + * Simple predicate to test if the modem reports a registered state. + */ + +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; +} + +/* ------------------------------------------------------------------------- + * State machine handlers + * - state enter/leave and per-state event handlers + * ------------------------------------------------------------------------- + */ + +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) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_BUS_OPENED: + modem_chat_attach(&data->chat, data->uart_pipe); + /* Run init script via chat TU wrapper (script symbols live in hl78xx_chat.c) */ + hl78xx_run_init_script_async(data); + 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; + } +} + +static int hl78xx_on_run_init_diagnose_script_state_enter(struct hl78xx_data *data) +{ + hl78xx_run_init_fail_script_async(data); + 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) { + hl78xx_run_enable_ksup_urc_script_async(data); + 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_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; + const char *cmd_restart = (const char *)SET_AIRPLANE_MODE_CMD; + + 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) { + ret = modem_dynamic_cmd_send(data, NULL, cmd_restart, strlen(cmd_restart), + hl78xx_get_ok_match(), 1, false); + if (ret < 0) { + goto error; + } + hl78xx_start_timer(data, + K_MSEC(config->shutdown_time_ms + config->startup_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; + + switch (evt) { + case MODEM_HL78XX_EVENT_TIMEOUT: + LOG_DBG("Rebooting modem to apply new RAT settings"); + ret = hl78xx_run_post_restart_script_async(data); + if (ret < 0) { + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_SUSPEND); + } + 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_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; + /* Apply the APN if not configured yet */ + if (data->status.apn.state == APN_STATE_REFRESH_REQUESTED) { + /* APN has been updated by the user, apply the new APN */ + HL78XX_LOG_DBG("APN refresh requested, applying new APN: \"%s\"", + data->identity.apn); + data->status.apn.state = APN_STATE_NOT_CONFIGURED; + } else { +#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, K_SECONDS(MDM_REGISTRATION_TIMEOUT)); + break; + + case MODEM_HL78XX_EVENT_TIMEOUT: + /** + * No need to run periodic script to check registration status because URC is used + * to notify the status change. + * + * If the modem is not registered within the timeout period, it will stay in this + * state forever. + * + * @attention MDM_REGISTRATION_TIMEOUT should be long enough to allow the modem to + * register to the network, especially for the first time registration. And also + * consider the network conditions / number of bands etc.. that may affect + * the registration process. + * + * TODO: add a mechanism to exit this state and retry the registration process + * + */ + LOG_WRN("Modem failed to register to the network within %d seconds", + MDM_REGISTRATION_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) +{ + notif_carrier_on(data->dev); + 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(data->dev, true); + 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(data->dev); + /* Check whether or not there is any sockets are connected, + * if true, wait until sockets are closed properly + */ + if (check_if_any_socket_connected(data->dev) == false) { + hl78xx_start_timer(data, K_MSEC(100)); + } else { + /* There are still active sockets, wait until they are closed */ + hl78xx_start_timer(data, K_MSEC(5000)); + } + 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; +} + +/* pwroff script moved to hl78xx_chat.c */ +static int hl78xx_on_init_power_off_state_enter(struct hl78xx_data *data) +{ + /** + * Eventhough you have power switch or etc.., start the power off script first + * to gracefully disconnect from the network + * + * IMSI detach before powering down IS recommended by the AT command manual + * + */ + return hl78xx_run_pwroff_script_async(data); +} + +static void hl78xx_init_power_off_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + switch (evt) { + case MODEM_HL78XX_EVENT_SCRIPT_SUCCESS: + 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; + } +} + +static int hl78xx_on_init_power_off_state_leave(struct hl78xx_data *data) +{ + 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_attach(data->uart_pipe, hl78xx_bus_pipe_handler, data); + 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_BUS_CLOSED: + break; + 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_FAIL_DIAGNOSTIC_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; + enum hl78xx_state s = data->status.state; + + /* Use an explicit bounds check against the last enum value so this + * code can reference the table even though the table is defined later + * in the file. MODEM_HL78XX_STATE_AWAIT_POWER_OFF is the last value in + * the `enum hl78xx_state`. + */ + if ((int)s <= MODEM_HL78XX_STATE_AWAIT_POWER_OFF && hl78xx_state_table[s].on_enter) { + ret = hl78xx_state_table[s].on_enter(data); + } + + return ret; +} + +static int hl78xx_on_state_leave(struct hl78xx_data *data) +{ + int ret = 0; + enum hl78xx_state s = data->status.state; + + if ((int)s <= MODEM_HL78XX_STATE_AWAIT_POWER_OFF && hl78xx_state_table[s].on_leave) { + ret = hl78xx_state_table[s].on_leave(data); + } + + 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_event_handler(struct hl78xx_data *data, enum hl78xx_event evt) +{ + enum hl78xx_state state; + enum hl78xx_state s; + + hl78xx_log_event(evt); + s = data->status.state; + state = data->status.state; + if ((int)s <= MODEM_HL78XX_STATE_AWAIT_POWER_OFF && hl78xx_state_table[s].on_event) { + hl78xx_state_table[s].on_event(data, evt); + } else { + LOG_ERR("%d %s unknown event", __LINE__, __func__); + } + if (state != s) { + hl78xx_log_state_changed(state, s); + } +} + +#ifdef CONFIG_PM_DEVICE + +/* ------------------------------------------------------------------------- + * Power management + * ------------------------------------------------------------------------- + */ + +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 PM_DEVICE_ACTION_SUSPEND", __LINE__); + 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 PM_DEVICE_ACTION_RESUME", __LINE__); + /* 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 PM_DEVICE_ACTION_TURN_ON", __LINE__); + break; + case PM_DEVICE_ACTION_TURN_OFF: + /* + * power off the device, used when the power + * domain this device belongs is suspended. + */ + LOG_DBG("%d PM_DEVICE_ACTION_TURN_OFF", __LINE__); + break; + default: + return -ENOTSUP; + } + return ret; +} +#endif /* CONFIG_PM_DEVICE */ + +/* ------------------------------------------------------------------------- + * Initialization + * ------------------------------------------------------------------------- + */ +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; + + k_mutex_init(&data->api_lock); + k_mutex_init(&data->tx_lock); + /* 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); +#ifdef CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING + k_sem_init(&data->stay_in_boot_mode_sem, 0, 1); +#endif /* CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING */ + k_sem_init(&data->script_stopped_sem_tx_int, 0, 1); + k_sem_init(&data->script_stopped_sem_rx_int, 0, 1); + data->dev = dev; + /* reset to default */ + data->buffers.eof_pattern_size = strlen(data->buffers.eof_pattern); + data->buffers.termination_pattern_size = strlen(data->buffers.termination_pattern); + memset(data->identity.apn, 0, MDM_APN_MAX_LENGTH); + /* GPIO validation */ + const struct gpio_dt_spec *gpio_pins[GPIO_CONFIG_LEN] = { +#if HAS_RESET_GPIO + &config->mdm_gpio_reset, +#endif +#if HAS_WAKE_GPIO + &config->mdm_gpio_wake, +#endif +#if HAS_VGPIO_GPIO + &config->mdm_gpio_vgpio, +#endif +#if HAS_UART_CTS_GPIO + &config->mdm_gpio_uart_cts, +#endif +#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_shutdown, +#endif +#if HAS_UART_DSR_GPIO + &config->mdm_gpio_uart_dsr, +#endif +#if HAS_UART_DTR_GPIO + &config->mdm_gpio_uart_dtr, +#endif +#if HAS_GPIO8_GPIO + &config->mdm_gpio_gpio8, +#endif +#if HAS_SIM_SWITCH_GPIO + &config->mdm_gpio_sim_switch, +#endif + }; + for (int i = 0; i < ARRAY_SIZE(gpio_pins); i++) { + if (gpio_pins[i] == NULL || !gpio_is_ready_dt(gpio_pins[i])) { + const char *port_name = "unknown"; + + if (gpio_pins[i] != NULL && gpio_pins[i]->port != NULL) { + port_name = gpio_pins[i]->port->name; + } + LOG_ERR("GPIO port (%s) not ready!", 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] = { +#if HAS_RESET_GPIO + {&config->mdm_gpio_reset, GPIO_OUTPUT, "reset"}, +#endif +#if HAS_WAKE_GPIO + {&config->mdm_gpio_wake, GPIO_OUTPUT, "wake"}, +#endif +#if HAS_VGPIO_GPIO + {&config->mdm_gpio_vgpio, GPIO_INPUT, "VGPIO"}, +#endif +#if HAS_UART_CTS_GPIO + {&config->mdm_gpio_uart_cts, GPIO_INPUT, "CTS"}, +#endif +#if HAS_GPIO6_GPIO + {&config->mdm_gpio_gpio6, GPIO_INPUT, "GPIO6"}, +#endif +#if HAS_PWR_ON_GPIO + {&config->mdm_gpio_pwr_on, GPIO_OUTPUT, "pwr_on"}, +#endif +#if HAS_FAST_SHUTD_GPIO + {&config->mdm_gpio_fast_shutdown, GPIO_OUTPUT, "fast_shutdown"}, +#endif +#if HAS_UART_DSR_GPIO + {&config->mdm_gpio_uart_dsr, GPIO_INPUT, "DSR"}, +#endif +#if HAS_UART_DTR_GPIO + {&config->mdm_gpio_uart_dtr, GPIO_OUTPUT, "DTR"}, +#endif +#if HAS_GPIO8_GPIO + {&config->mdm_gpio_gpio8, GPIO_INPUT, "GPIO8"}, +#endif +#if HAS_SIM_SWITCH_GPIO + {&config->mdm_gpio_sim_switch, GPIO_INPUT, "SIM_SWITCH"}, +#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; + } + } +#if HAS_VGPIO_GPIO + /* 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; + } +#endif /* HAS_VGPIO_GPIO */ +#if HAS_GPIO6_GPIO + /* 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 */ + /* UART pipe initialization */ + (void)hl78xx_init_pipe(dev); + + ret = modem_init_chat(dev); + if (ret < 0) { + goto error; + } + +#ifndef CONFIG_PM_DEVICE + hl78xx_delegate_event(data, MODEM_HL78XX_EVENT_RESUME); +#else + pm_device_init_suspended(dev); +#endif /* CONFIG_PM_DEVICE */ + +#ifdef CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING + k_sem_take(&data->stay_in_boot_mode_sem, K_FOREVER); +#endif + return 0; +error: + return ret; +} + +int hl78xx_evt_notif_handler_set(hl78xx_evt_monitor_dispatcher_t handler) +{ + event_dispatcher = handler; + return 0; +} + +/* + * State handler table + * Maps each hl78xx_state to optional enter/leave/event handlers. NULL + * entries mean the state has no action for that phase. + */ + +/* clang-format off */ +const static struct hl78xx_state_handlers hl78xx_state_table[] = { + [MODEM_HL78XX_STATE_IDLE] = { + hl78xx_on_idle_state_enter, + hl78xx_on_idle_state_leave, + hl78xx_idle_event_handler + }, + [MODEM_HL78XX_STATE_RESET_PULSE] = { + hl78xx_on_reset_pulse_state_enter, + hl78xx_on_reset_pulse_state_leave, + hl78xx_reset_pulse_event_handler + }, + [MODEM_HL78XX_STATE_POWER_ON_PULSE] = { + hl78xx_on_power_on_pulse_state_enter, + hl78xx_on_power_on_pulse_state_leave, + hl78xx_power_on_pulse_event_handler + }, + [MODEM_HL78XX_STATE_AWAIT_POWER_ON] = { + hl78xx_on_await_power_on_state_enter, + NULL, + hl78xx_await_power_on_event_handler + }, + [MODEM_HL78XX_STATE_SET_BAUDRATE] = { + NULL, + NULL, + NULL + }, + [MODEM_HL78XX_STATE_RUN_INIT_SCRIPT] = { + hl78xx_on_run_init_script_state_enter, + NULL, + hl78xx_run_init_script_event_handler + }, + [MODEM_HL78XX_STATE_RUN_INIT_FAIL_DIAGNOSTIC_SCRIPT] = { + hl78xx_on_run_init_diagnose_script_state_enter, + NULL, + hl78xx_run_init_fail_script_event_handler + }, + [MODEM_HL78XX_STATE_RUN_RAT_CONFIG_SCRIPT] = { + hl78xx_on_rat_cfg_script_state_enter, + NULL, + hl78xx_run_rat_cfg_script_event_handler + }, + [MODEM_HL78XX_STATE_RUN_ENABLE_GPRS_SCRIPT] = { + hl78xx_on_enable_gprs_state_enter, + NULL, + hl78xx_enable_gprs_event_handler + }, + [MODEM_HL78XX_STATE_AWAIT_REGISTERED] = { + hl78xx_on_await_registered_state_enter, + hl78xx_on_await_registered_state_leave, + hl78xx_await_registered_event_handler + }, + [MODEM_HL78XX_STATE_CARRIER_ON] = { + hl78xx_on_carrier_on_state_enter, + hl78xx_on_carrier_on_state_leave, + hl78xx_carrier_on_event_handler + }, + [MODEM_HL78XX_STATE_CARRIER_OFF] = { + hl78xx_on_carrier_off_state_enter, + hl78xx_on_carrier_off_state_leave, + hl78xx_carrier_off_event_handler + }, + [MODEM_HL78XX_STATE_SIM_POWER_OFF] = { + NULL, + NULL, + NULL + }, + [MODEM_HL78XX_STATE_AIRPLANE] = { + NULL, + NULL, + NULL + }, + [MODEM_HL78XX_STATE_INIT_POWER_OFF] = { + hl78xx_on_init_power_off_state_enter, + hl78xx_on_init_power_off_state_leave, + hl78xx_init_power_off_event_handler + }, + [MODEM_HL78XX_STATE_POWER_OFF_PULSE] = { + hl78xx_on_power_off_pulse_state_enter, + hl78xx_on_power_off_pulse_state_leave, + hl78xx_power_off_pulse_event_handler + }, + [MODEM_HL78XX_STATE_AWAIT_POWER_OFF] = { + hl78xx_on_await_power_off_state_enter, + NULL, + hl78xx_await_power_off_event_handler + }, +}; +/* clang-format on */ +static DEVICE_API(cellular, hl78xx_api) = { + .get_signal = hl78xx_api_func_get_signal, + .get_modem_info = hl78xx_api_func_get_modem_info_standard, + .get_registration_status = hl78xx_api_func_get_registration_status, + .set_apn = hl78xx_api_func_set_apn, + .set_callback = NULL, +}; +/* ------------------------------------------------------------------------- + * Device API and DT registration + * ------------------------------------------------------------------------- + */ +#define MODEM_HL78XX_DEFINE_INSTANCE(inst, power_ms, reset_ms, startup_ms, shutdown_ms, start, \ + 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, {}), \ + .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_shutdown = \ + 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), \ + .periodic_chat_script = (periodic_script), \ + }; \ + static struct hl78xx_data hl78xx_data_##inst = { \ + .buffers.delimiter = "\r\n", \ + .buffers.eof_pattern = EOF_PATTERN, \ + .buffers.termination_pattern = TERMINATION_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, 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, NULL, NULL) + +#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 0000000000000..d48f5b2d88743 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx.h @@ -0,0 +1,652 @@ +/* + * 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 CONFIG_MODEM_HL78XX_NUM_SOCKETS +#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 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 TERMINATION_PATTERN "+++" +#define CONNECT_STRING "CONNECT" +#define CME_ERROR_STRING "+CME ERROR: " +#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 SET_SIM_PWR_OFF_MODE_CMD "AT+CFUN=0" +#define GET_FULLFUNCTIONAL_MODE_CMD "AT+CFUN?" +#define MDM_POWER_OFF_CMD_LEGACY "AT+CPWROFF" +#define MDM_POWER_FAST_OFF_CMD_LEGACY "AT+CPWROFF=1" +/* 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__) +#define ATOD(s_, value_, desc_) modem_atod(s_, value_, desc_, __func__) + +#define HL78XX_LOG_DBG(str, ...) \ + COND_CODE_1(CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG, \ + (LOG_DBG(str, ##__VA_ARGS__)), \ + ((void)0)) + +/* Enums */ +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, +}; + +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 representing information transfer capability events */ +enum hl78xx_info_transfer_event { + EVENT_START_SCAN = 0, + EVENT_FAIL_SCAN, + EVENT_ENTER_CAMPED, + EVENT_CONNECTION_ESTABLISHMENT, + EVENT_START_RESCAN, + EVENT_RRC_CONNECTED, + EVENT_NO_SUITABLE_CELLS, + EVENT_ALL_REGISTRATION_FAILED +}; + +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]; +}; + +enum apn_state_enum_t { + APN_STATE_NOT_CONFIGURED = 0, + APN_STATE_CONFIGURED, + APN_STATE_REFRESH_REQUESTED, + APN_STATE_REFRESH_IN_PROGRESS, + APN_STATE_REFRESH_COMPLETED, +}; + +struct apn_state { + enum apn_state_enum_t state; +}; +struct registration_status { + bool is_registered_currently; + bool is_registered_previously; + enum cellular_registration_status network_state_current; + enum cellular_registration_status network_state_previous; + 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; + uint8_t *termination_pattern; + uint8_t termination_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 hl78xx_phone_functionality_work { + enum hl78xx_phone_functionality functionality; + bool in_progress; +}; + +struct hl78xx_network_operator { + char operator[MDM_MODEL_LENGTH]; + uint8_t format; +}; + +struct modem_status { + struct registration_status registration; + int16_t rssi; + uint8_t ksrep; + int16_t rsrp; + int16_t rsrq; + uint16_t script_fail_counter; + int variant; + enum hl78xx_state state; + struct kband_syntax kbndcfg[HL78XX_RAT_COUNT]; + struct hl78xx_phone_functionality_work phone_functionality; + struct apn_state apn; + struct hl78xx_network_operator network_operator; +}; + +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_mutex api_lock; + struct k_sem script_stopped_sem_tx_int; + struct k_sem script_stopped_sem_rx_int; + struct k_sem suspended_sem; +#ifdef CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING + struct k_sem stay_in_boot_mode_sem; +#endif /* CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING */ + + 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; + /* Track leftover socket data state previously stored as a TU-global. + * Moving this into the per-modem data reduces global BSS and keeps + * state colocated with the modem instance. + */ + atomic_t state_leftover; +#if defined(CONFIG_MODEM_HL78XX_RSSI_WORK) + struct k_work_delayable rssi_query_work; +#endif + + const struct device *dev; + /* GNSS device */ + const struct device *gnss_dev; + /* Offload device */ + const struct device *offload_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_vgpio; + struct gpio_dt_spec mdm_gpio_uart_cts; + struct gpio_dt_spec mdm_gpio_gpio6; + struct gpio_dt_spec mdm_gpio_fast_shutdown; + struct gpio_dt_spec mdm_gpio_uart_dtr; + struct gpio_dt_spec mdm_gpio_uart_dsr; + 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; + const struct modem_chat_script *periodic_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 DNS resolution work callback. + * + * @param dev Pointer to the device structure. + * @param hard_reset Boolean indicating if a hard reset is required. + * Should be used internally to handle DNS resolution events. + */ +void dns_work_cb(const struct device *dev, bool hard_reset); + +/** + * @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 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_dynamic_cmd_send(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); + +#define HASH_MULTIPLIER 37 +/** + * @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. + */ +static inline 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; +} + +/** + * @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 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 + */ +static inline 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') { + return err_value; + } + return ret; +} + +/** + * @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. + */ +static inline 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') { + return err_value; + } + return ret; +} +/** + * @brief Small utility: safe strncpy that always NUL-terminates the destination. + * This function copies a string from src to dst, ensuring that the destination + * buffer is always NUL-terminated, even if the source string is longer than + * the destination buffer. + * @param dst Destination buffer. + * @param src Source string. + * @param dst_size Size of the destination buffer. + */ +static inline void safe_strncpy(char *dst, const char *src, size_t dst_size) +{ + size_t len = 0; + + if (dst == NULL || dst_size == 0) { + return; + } + if (src == NULL) { + dst[0] = '\0'; + return; + } + len = strlen(src); + if (len >= dst_size) { + len = dst_size - 1; + } + memcpy(dst, src, len); + dst[len] = '\0'; +} + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG +/** + * @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, int rat_mode); +#endif + +#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 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); + +/** + * @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 cellular_access_technology tech, + enum cellular_registration_status *status); + +/** + * @brief hl78xx_api_func_set_apn - Brief description of the function. + * @param dev Description of dev. + * @param apn Description of apn. + * @return int Description of return value. + */ +int hl78xx_api_func_set_apn(const struct device *dev, const char *apn); + +/** + * @brief hl78xx_api_func_get_modem_info_standard - 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_standard(const struct device *dev, + enum cellular_modem_info_type type, char *info, + size_t 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 dev Description of dev. + */ +void notif_carrier_off(const struct device *dev); + +/** + * @brief notif_carrier_on - Brief description of the function. + * @param dev Description of dev. + */ +void notif_carrier_on(const struct device *dev); + +/** + * @brief check_if_any_socket_connected - Brief description of the function. + * @param dev Description of dev. + * @return int Description of return value. + */ +int check_if_any_socket_connected(const struct device *dev); + +#endif /* HL78XX_H */ diff --git a/drivers/modem/hl78xx/hl78xx_apis.c b/drivers/modem/hl78xx/hl78xx_apis.c new file mode 100644 index 0000000000000..e1af412b1730c --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_apis.c @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include +#include +#include +#include +#include +#include "hl78xx.h" +#include "hl78xx_chat.h" + +LOG_MODULE_REGISTER(hl78xx_apis, CONFIG_MODEM_LOG_LEVEL); + +/* Wrapper to centralize modem_dynamic_cmd_send calls and reduce repetition. + * returns negative errno on failure or the value returned by modem_dynamic_cmd_send. + */ +static int hl78xx_send_cmd(struct hl78xx_data *data, const char *cmd, + void (*chat_cb)(struct modem_chat *, enum modem_chat_script_result, + void *), + const struct modem_chat_match *matches, uint16_t match_count) +{ + if (data == NULL || cmd == NULL) { + return -EINVAL; + } + return modem_dynamic_cmd_send(data, chat_cb, cmd, (uint16_t)strlen(cmd), matches, + match_count, true); +} + +int hl78xx_api_func_get_signal(const struct device *dev, const enum cellular_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"; + + /* quick check of state under api_lock */ + k_mutex_lock(&data->api_lock, K_FOREVER); + if (data->status.state != MODEM_HL78XX_STATE_CARRIER_ON) { + k_mutex_unlock(&data->api_lock); + return -ENODATA; + } + k_mutex_unlock(&data->api_lock); + + /* Run chat script */ + switch (type) { + case CELLULAR_SIGNAL_RSSI: + ret = hl78xx_send_cmd(data, signal_cmd_csq, NULL, hl78xx_get_allow_match(), + hl78xx_get_allow_match_size()); + break; + + case CELLULAR_SIGNAL_RSRP: + case CELLULAR_SIGNAL_RSRQ: + ret = hl78xx_send_cmd(data, signal_cmd_cesq, NULL, hl78xx_get_allow_match(), + hl78xx_get_allow_match_size()); + break; + + default: + ret = -ENOTSUP; + break; + } + /* Verify chat script ran successfully */ + if (ret < 0) { + return ret; + } + /* Parse received value */ + switch (type) { + case CELLULAR_SIGNAL_RSSI: + ret = hl78xx_parse_rssi(data->status.rssi, value); + break; + + case CELLULAR_SIGNAL_RSRP: + ret = hl78xx_parse_rsrp(data->status.rsrp, value); + break; + + case CELLULAR_SIGNAL_RSRQ: + ret = hl78xx_parse_rsrq(data->status.rsrq, value); + break; + + default: + ret = -ENOTSUP; + break; + } + return ret; +} + +/** Convert hl78xx RAT mode to cellular access technology */ +enum cellular_access_technology hl78xx_rat_to_access_tech(enum hl78xx_cell_rat_mode rat_mode) +{ + switch (rat_mode) { + case HL78XX_RAT_CAT_M1: + return CELLULAR_ACCESS_TECHNOLOGY_E_UTRAN; + case HL78XX_RAT_NB1: + return CELLULAR_ACCESS_TECHNOLOGY_E_UTRAN_NB_S1; +#ifdef CONFIG_MODEM_HL78XX_12 + case HL78XX_RAT_GSM: + return CELLULAR_ACCESS_TECHNOLOGY_GSM; +#ifdef CONFIG_MODEM_HL78XX_12_FW_R6 + case HL78XX_RAT_NBNTN: + /** NBNTN might not have a direct mapping; choose closest or define new */ + return CELLULAR_ACCESS_TECHNOLOGY_NG_RAN_SAT; +#endif +#endif +#ifdef CONFIG_MODEM_HL78XX_AUTORAT + case HL78XX_RAT_MODE_AUTO: + /** AUTO mode doesn't map directly; return LTE as default or NONE */ + return CELLULAR_ACCESS_TECHNOLOGY_E_UTRAN; +#endif + case HL78XX_RAT_MODE_NONE: + default: + return -ENODATA; + } +} + +int hl78xx_api_func_get_registration_status(const struct device *dev, + enum cellular_access_technology tech, + enum cellular_registration_status *status) +{ + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + + if (status == NULL) { + return -EINVAL; + } + LOG_DBG("Requested tech: %d, current rat mode: %d REG: %d %d", tech, + data->status.registration.rat_mode, data->status.registration.network_state_current, + hl78xx_rat_to_access_tech(data->status.registration.rat_mode)); + if (tech != hl78xx_rat_to_access_tech(data->status.registration.rat_mode)) { + return -ENODATA; + } + k_mutex_lock(&data->api_lock, K_FOREVER); + *status = data->status.registration.network_state_current; + k_mutex_unlock(&data->api_lock); + return 0; +} + +int hl78xx_api_func_get_modem_info_vendor(const struct device *dev, + enum hl78xx_modem_info_type type, void *info, size_t size) +{ + int ret = 0; + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + const char *network_operator = "AT+COPS?"; + + if (info == NULL || size == 0) { + return -EINVAL; + } + /* copy identity under api lock to a local buffer then write to caller + * prevents holding lock during the return/caller access + */ + k_mutex_lock(&data->api_lock, K_FOREVER); + switch (type) { + case HL78XX_MODEM_INFO_APN: + if (data->status.apn.state != APN_STATE_CONFIGURED) { + ret = -ENODATA; + break; + } + safe_strncpy(info, (const char *)data->identity.apn, size); + break; + + case HL78XX_MODEM_INFO_CURRENT_RAT: + *(enum hl78xx_cell_rat_mode *)info = data->status.registration.rat_mode; + break; + + case HL78XX_MODEM_INFO_NETWORK_OPERATOR: + /* Network operator not currently tracked; return empty or implement tracking */ + ret = hl78xx_send_cmd(data, network_operator, NULL, hl78xx_get_allow_match(), + hl78xx_get_allow_match_size()); + if (ret < 0) { + LOG_ERR("Failed to get network operator"); + } + safe_strncpy(info, (const char *)data->status.network_operator.operator, + MIN(size, sizeof(data->status.network_operator.operator))); + break; + + default: + break; + } + k_mutex_unlock(&data->api_lock); + return ret; +} + +int hl78xx_api_func_get_modem_info_standard(const struct device *dev, + enum cellular_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; + } + /* copy identity under api lock to a local buffer then write to caller + * prevents holding lock during the return/caller access + */ + k_mutex_lock(&data->api_lock, K_FOREVER); + switch (type) { + case CELLULAR_MODEM_INFO_IMEI: + safe_strncpy(info, (const char *)data->identity.imei, + MIN(size, sizeof(data->identity.imei))); + break; + case CELLULAR_MODEM_INFO_SIM_IMSI: + safe_strncpy(info, (const char *)data->identity.imsi, + MIN(size, sizeof(data->identity.imsi))); + break; + case CELLULAR_MODEM_INFO_MANUFACTURER: + safe_strncpy(info, (const char *)data->identity.manufacturer, + MIN(size, sizeof(data->identity.manufacturer))); + break; + case CELLULAR_MODEM_INFO_FW_VERSION: + safe_strncpy(info, (const char *)data->identity.fw_version, + MIN(size, sizeof(data->identity.fw_version))); + break; + case CELLULAR_MODEM_INFO_MODEL_ID: + safe_strncpy(info, (const char *)data->identity.model_id, + MIN(size, sizeof(data->identity.model_id))); + break; + case CELLULAR_MODEM_INFO_SIM_ICCID: + safe_strncpy(info, (const char *)data->identity.iccid, + MIN(size, sizeof(data->identity.iccid))); + break; + default: + ret = -ENOTSUP; + break; + } + k_mutex_unlock(&data->api_lock); + return ret; +} + +int hl78xx_api_func_set_apn(const struct device *dev, const char *apn) +{ + struct hl78xx_data *data = (struct hl78xx_data *)dev->data; + /** + * Validate APN + * APN can be empty string to clear it + * to request it from network + * If the value is null or omitted, then the subscription + * value will be requested + */ + if (apn == NULL) { + return -EINVAL; + } + if (strlen(apn) >= MDM_APN_MAX_LENGTH) { + return -EINVAL; + } + /* Update in-memory APN under api lock */ + k_mutex_lock(&data->api_lock, K_FOREVER); + safe_strncpy(data->identity.apn, apn, sizeof(data->identity.apn)); + data->status.apn.state = APN_STATE_REFRESH_REQUESTED; + k_mutex_unlock(&data->api_lock); + 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 functionality with/without restart */ + snprintf(cmd_string, sizeof(cmd_string), "AT+CFUN=%d,%d", functionality, reset); + return hl78xx_send_cmd(data, cmd_string, NULL, hl78xx_get_ok_match(), 1); +} + +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; + /* get modem phone functionality */ + return hl78xx_send_cmd(data, cmd_string, NULL, hl78xx_get_ok_match(), 1); +} + +int hl78xx_api_func_modem_dynamic_cmd_send(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; + + if (cmd == NULL) { + return -EINVAL; + } + /* respect provided matches_size and serialize modem access */ + return modem_dynamic_cmd_send(data, NULL, cmd, cmd_size, response_matches, matches_size, + true); +} diff --git a/drivers/modem/hl78xx/hl78xx_cfg.c b/drivers/modem/hl78xx/hl78xx_cfg.c new file mode 100644 index 0000000000000..461d066865b78 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_cfg.c @@ -0,0 +1,587 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * hl78xx_cfg.c + * + * Extracted helper implementations for RAT, band and APN configuration to + * keep the main state-machine TU small and maintainable. + */ +#include "hl78xx.h" +#include "hl78xx_cfg.h" +#include "hl78xx_chat.h" +#include + +LOG_MODULE_DECLARE(hl78xx_dev); + +#define ICCID_PREFIX_LEN 7 +#define IMSI_PREFIX_LEN 6 +#define MAX_BANDS 32 +#define MDM_APN_FULL_STRING_MAX_LEN 256 + +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; + + ret = modem_dynamic_cmd_send(data, NULL, cmd_kselq, strlen(cmd_kselq), + hl78xx_get_ok_match(), 1, false); + if (ret < 0) { + goto error; + } else { + *modem_require_restart = true; + } + } + + *rat_request = HL78XX_RAT_MODE_AUTO; +#else + char const *cmd_ksrat_query = (const char *)KSRAT_QUERY; + char const *cmd_kselq_disable = (const char *)DISABLE_RAT_AUTO; + const char *cmd_set_rat = NULL; + /* Check if auto rat are disabled */ + if (data->kselacq_data.rat1 != 0 && data->kselacq_data.rat2 != 0 && + data->kselacq_data.rat3 != 0) { + ret = modem_dynamic_cmd_send(data, NULL, cmd_kselq_disable, + strlen(cmd_kselq_disable), hl78xx_get_ok_match(), 1, + false); + if (ret < 0) { + goto error; + } + } + /* Query current rat */ + ret = modem_dynamic_cmd_send(data, NULL, cmd_ksrat_query, strlen(cmd_ksrat_query), + hl78xx_get_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 + + 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_HL78XX_12 + 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_HL78XX_12_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 +#endif + + 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_dynamic_cmd_send(data, NULL, cmd_set_rat, strlen(cmd_set_rat), + hl78xx_get_ok_match(), 1, false); + if (ret < 0) { + goto error; + } else { + *modem_require_restart = true; + } + } +#endif + +error: + return ret; +} + +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}; + const char *modem_trimmed; + const char *expected_trimmed; + + if (rat_config_request == HL78XX_RAT_MODE_NONE) { + return -EINVAL; + } +#ifdef CONFIG_MODEM_HL78XX_AUTORAT + for (int rat = HL78XX_RAT_CAT_M1; rat <= HL78XX_RAT_NB1; rat++) { +#else + 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; + } + modem_trimmed = hl78xx_trim_leading_zeros(data->status.kbndcfg[rat].bnd_bitmap); + 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); + ret = modem_dynamic_cmd_send(data, NULL, cmd_bnd, strlen(cmd_bnd), + hl78xx_get_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; +} + +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) + + MODEM_HL78XX_ADDRESS_FAMILY_FORMAT_LEN + 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; + } + + k_mutex_lock(&data->api_lock, K_FOREVER); + if (strncmp(data->identity.apn, apn, apn_size) != 0) { + safe_strncpy(data->identity.apn, apn, sizeof(data->identity.apn)); + } + k_mutex_unlock(&data->api_lock); + + snprintk(cmd_string, cmd_max_len, "AT+CGDCONT=1,\"%s\",\"%s\"", MODEM_HL78XX_ADDRESS_FAMILY, + apn); + + ret = modem_dynamic_cmd_send(data, NULL, cmd_string, strlen(cmd_string), + hl78xx_get_ok_match(), 1, false); + if (ret < 0) { + goto error; + } + snprintk(cmd_string, cmd_max_len, + "AT+KCNXCFG=1,\"GPRS\",\"%s\",,,\"" MODEM_HL78XX_ADDRESS_FAMILY "\"", apn); + ret = modem_dynamic_cmd_send(data, NULL, cmd_string, strlen(cmd_string), + hl78xx_get_ok_match(), 1, false); + if (ret < 0) { + goto error; + } + data->status.apn.state = APN_STATE_CONFIGURED; + return 0; +error: + LOG_ERR("Set APN to %s, result: %d", apn, ret); + return ret; +} + +#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) +{ + uint16_t bit_pos; + uint16_t byte_index; + uint8_t bit_index; + + if (band_num < 1 || band_num > 256) { + return; /* Out of range */ + } + /* Calculate byte and bit positions */ + bit_pos = band_num - 1; + byte_index = bit_pos / 8; + bit_index = bit_pos % 8; + /* Big-endian format: band 1 in byte 31, band 256 in byte 0 */ + bitmap[byte_index] |= (1 << bit_index); +} + +#ifdef CONFIG_MODEM_HL78XX_CONFIGURE_BANDS +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; +} +#endif /* CONFIG_MODEM_HL78XX_CONFIGURE_BANDS */ + +#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) +{ + char buf[128] = {0}; + char *token; + char *rest; + int count = 0; + int band = 0; + + if (!band_str || !bands || max_bands == 0) { + return -EINVAL; + } + strncpy(buf, band_str, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + rest = buf; + while ((token = strtok_r(rest, ",", &rest))) { + band = ATOI(token, -1, "band"); + 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; + } +#else + /* Else: use standalone config */ + return hl78xx_generate_band_bitmap(bitmap_out); +#endif /* CONFIG_MODEM_HL78XX_AUTORAT */ + return -EINVAL; +} + +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[MDM_APN_FULL_STRING_MAX_LEN] = {0}; + size_t len; + const char *mnc_ptr; + + if (full_apn == NULL || essential_apn == NULL || max_len == 0) { + return; + } + 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); + mnc_ptr = strstr(apn_buf, ".mnc"); + 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/drivers/modem/hl78xx/hl78xx_cfg.h b/drivers/modem/hl78xx/hl78xx_cfg.h new file mode 100644 index 0000000000000..5d8be2abe58d5 --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_cfg.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * hl78xx_cfg.h + * + * Helper APIs for RAT, band and APN configuration extracted from hl78xx.c + * to keep the state machine file smaller and easier to read. + */ +#ifndef ZEPHYR_DRIVERS_MODEM_HL78XX_HL78XX_CFG_H_ +#define ZEPHYR_DRIVERS_MODEM_HL78XX_HL78XX_CFG_H_ + +#include +#include +#include "hl78xx.h" + +int hl78xx_rat_cfg(struct hl78xx_data *data, bool *modem_require_restart, + enum hl78xx_cell_rat_mode *rat_request); + +int hl78xx_band_cfg(struct hl78xx_data *data, bool *modem_require_restart, + enum hl78xx_cell_rat_mode rat_config_request); + +int hl78xx_set_apn_internal(struct hl78xx_data *data, const char *apn, uint16_t size); + +/** + * @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 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 hl78xx_extract_essential_part_apn - Extract the essential part of the APN. + * @param full_apn Full APN string. + * @param essential_apn Buffer to store the essential part of the APN. + * @param max_len Maximum length of the essential APN buffer. + */ +void hl78xx_extract_essential_part_apn(const char *full_apn, char *essential_apn, size_t max_len); + +#endif /* ZEPHYR_DRIVERS_MODEM_HL78XX_HL78XX_CFG_H_ */ diff --git a/drivers/modem/hl78xx/hl78xx_chat.c b/drivers/modem/hl78xx/hl78xx_chat.c new file mode 100644 index 0000000000000..5b94791aa1d2e --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_chat.c @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + ***************************************************************************** + * hl78xx_chat.c + * + * Centralized translation unit for MODEM_CHAT_* macro-generated objects and + * chat scripts for the HL78xx driver. This file contains the MODEM_CHAT + * matches and script definitions and exposes runtime wrapper functions + * declared in hl78xx_chat.h. + * + * Contract: + * - Other translation units MUST NOT take addresses of the MODEM_CHAT_* + * symbols or use ARRAY_SIZE() on them at file scope. Use the getters + * (hl78xx_get_*) and runners (hl78xx_ run_*_script[_async]) instead. + ***************************************************************************** + */ + +#include "hl78xx.h" +#include "hl78xx_chat.h" +#include +#include + +LOG_MODULE_DECLARE(hl78xx_dev); + +/* Forward declarations of handlers implemented in hl78xx.c (extern linkage) */ +void hl78xx_on_cxreg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +/* +CGCONTRDP handler implemented in hl78xx_sockets.c - declared here so the + * chat match may reference it. This handler parses PDP context response and + * updates DNS / interface state for the driver instance. + */ +void hl78xx_on_cgdcontrdp(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +#if defined(CONFIG_MODEM_HL78XX_12) +void hl78xx_on_kstatev(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +#endif +void hl78xx_on_socknotifydata(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_ktcpnotif(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +/* Handler implemented to assign modem-provided udp socket ids */ +void hl78xx_on_kudpsocket_create(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data); +void hl78xx_on_ktcpsocket_create(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data); +/* Handler implemented to assign modem-provided tcp socket ids */ +void hl78xx_on_ktcpind(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +/* + * Chat script and URC match definitions - extracted from hl78xx.c + */ +void hl78xx_on_udprcv(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_kbndcfg(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_csq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_cesq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_cfun(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_cops(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_ksup(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_imei(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_cgmm(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_imsi(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_cgmi(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_cgmr(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_iccid(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_ksrep(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_ksrat(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); +void hl78xx_on_kselacq(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data); + +MODEM_CHAT_MATCH_DEFINE(hl78xx_ok_match, "OK", "", NULL); +MODEM_CHAT_MATCHES_DEFINE(hl78xx_allow_match, MODEM_CHAT_MATCH("OK", "", NULL), + MODEM_CHAT_MATCH(CME_ERROR_STRING, "", NULL)); + +MODEM_CHAT_MATCHES_DEFINE(hl78xx_unsol_matches, MODEM_CHAT_MATCH("+CREG: ", ",", hl78xx_on_cxreg), + MODEM_CHAT_MATCH("+CEREG: ", ",", hl78xx_on_cxreg), +#if defined(CONFIG_MODEM_HL78XX_12) + MODEM_CHAT_MATCH("+KSTATEV: ", ",", hl78xx_on_kstatev), +#endif + MODEM_CHAT_MATCH("+KUDP_DATA: ", ",", hl78xx_on_socknotifydata), + MODEM_CHAT_MATCH("+KTCP_DATA: ", ",", hl78xx_on_socknotifydata), + MODEM_CHAT_MATCH("+KTCP_NOTIF: ", ",", hl78xx_on_ktcpnotif), +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + MODEM_CHAT_MATCH("+KUDP_RCV: ", ",", hl78xx_on_udprcv), +#endif + 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("+COPS: ", ",", hl78xx_on_cops)); + +MODEM_CHAT_MATCHES_DEFINE(hl78xx_abort_matches, MODEM_CHAT_MATCH("+CME ERROR: ", "", NULL)); +MODEM_CHAT_MATCH_DEFINE(hl78xx_at_ready_match, "+KSUP: ", "", hl78xx_on_ksup); +MODEM_CHAT_MATCH_DEFINE(hl78xx_imei_match, "", "", hl78xx_on_imei); +MODEM_CHAT_MATCH_DEFINE(hl78xx_cgmm_match, "", "", hl78xx_on_cgmm); +MODEM_CHAT_MATCH_DEFINE(hl78xx_cimi_match, "", "", hl78xx_on_imsi); +MODEM_CHAT_MATCH_DEFINE(hl78xx_cgmi_match, "", "", hl78xx_on_cgmi); +MODEM_CHAT_MATCH_DEFINE(hl78xx_cgmr_match, "", "", hl78xx_on_cgmr); +MODEM_CHAT_MATCH_DEFINE(hl78xx_iccid_match, "+CCID: ", "", hl78xx_on_iccid); +MODEM_CHAT_MATCH_DEFINE(hl78xx_ksrep_match, "+KSREP: ", ",", hl78xx_on_ksrep); +MODEM_CHAT_MATCH_DEFINE(hl78xx_ksrat_match, "+KSRAT: ", "", hl78xx_on_ksrat); +MODEM_CHAT_MATCH_DEFINE(hl78xx_kselacq_match, "+KSELACQ: ", ",", hl78xx_on_kselacq); + +/* Chat script matches / definitions */ +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_periodic_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG?", hl78xx_ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_periodic_chat_script, hl78xx_periodic_chat_script_cmds, + hl78xx_abort_matches, hl78xx_chat_callback_handler, 4); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_init_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_at_ready_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KHWIOCFG=3,1,6", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("ATE0", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CFUN=4,0", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSLEEP=2", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPSMS=0", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEDRXS=0", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KPATTERN=\"--EOF--Pattern--\"", + hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CCID", hl78xx_iccid_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CMEE=1", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGSN", hl78xx_imei_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGMM", hl78xx_cgmm_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGMI", hl78xx_cgmi_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGMR", hl78xx_cgmr_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CIMI", hl78xx_cimi_match), + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_ok_match), +#if defined(CONFIG_MODEM_HL78XX_12) + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSTATEV=1", hl78xx_ok_match), +#endif + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGEREP=2", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSELACQ?", hl78xx_kselacq_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSRAT?", hl78xx_ksrat_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KBNDCFG?", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CGACT?", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CREG=0", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CEREG=5", hl78xx_ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_init_chat_script, hl78xx_init_chat_script_cmds, + hl78xx_abort_matches, hl78xx_chat_callback_handler, 10); + +/* Post-restart script (moved from hl78xx.c) */ +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_post_restart_chat_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("", hl78xx_at_ready_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSRAT?", hl78xx_ksrat_match), +#if defined(CONFIG_MODEM_HL78XX_12) + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSTATEV=1", hl78xx_ok_match) +#endif +); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_post_restart_chat_script, hl78xx_post_restart_chat_script_cmds, + hl78xx_abort_matches, hl78xx_chat_callback_handler, 1000); + +/* init_fail_script moved from hl78xx.c */ +MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_fail_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP?", hl78xx_ksrep_match)); + +MODEM_CHAT_SCRIPT_DEFINE(init_fail_script, init_fail_script_cmds, hl78xx_abort_matches, + hl78xx_chat_callback_handler, 10); + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_enable_ksup_urc_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP=1", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+KSREP?", hl78xx_ksrep_match)); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_enable_ksup_urc_script, hl78xx_enable_ksup_urc_cmds, + hl78xx_abort_matches, hl78xx_chat_callback_handler, 4); + +/* power-off script moved from hl78xx.c */ +MODEM_CHAT_SCRIPT_CMDS_DEFINE(hl78xx_pwroff_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CFUN=0", hl78xx_ok_match), + MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPWROFF", hl78xx_ok_match)); + +MODEM_CHAT_SCRIPT_DEFINE(hl78xx_pwroff_script, hl78xx_pwroff_cmds, hl78xx_abort_matches, + hl78xx_chat_callback_handler, 4); + +/* Socket-specific matches and wrappers exposed for the sockets translation + * unit. These were extracted from hl78xx_sockets.c to centralize chat + * definitions. + */ +MODEM_CHAT_MATCHES_DEFINE(connect_matches, MODEM_CHAT_MATCH(CONNECT_STRING, "", NULL), + MODEM_CHAT_MATCH(CME_ERROR_STRING, "", NULL)); +MODEM_CHAT_MATCH_DEFINE(kudpind_match, "+KUDP_IND: ", ",", hl78xx_on_kudpsocket_create); +MODEM_CHAT_MATCH_DEFINE(ktcpind_match, "+KTCP_IND: ", ",", hl78xx_on_ktcpind); +MODEM_CHAT_MATCH_DEFINE(ktcpcfg_match, "+KTCPCFG: ", "", hl78xx_on_ktcpsocket_create); +MODEM_CHAT_MATCH_DEFINE(cgdcontrdp_match, "+CGCONTRDP: ", ",", hl78xx_on_cgdcontrdp); +MODEM_CHAT_MATCH_DEFINE(ktcp_state_match, "+KTCPSTAT: ", ",", NULL); + +const struct modem_chat_match *hl78xx_get_sockets_ok_match(void) +{ + return &hl78xx_ok_match; +} + +const struct modem_chat_match *hl78xx_get_connect_matches(void) +{ + return connect_matches; +} + +size_t hl78xx_get_connect_matches_size(void) +{ + return (size_t)ARRAY_SIZE(connect_matches); +} + +const struct modem_chat_match *hl78xx_get_sockets_allow_matches(void) +{ + return hl78xx_allow_match; +} + +size_t hl78xx_get_sockets_allow_matches_size(void) +{ + return (size_t)ARRAY_SIZE(hl78xx_allow_match); +} + +const struct modem_chat_match *hl78xx_get_kudpind_match(void) +{ + return &kudpind_match; +} + +const struct modem_chat_match *hl78xx_get_ktcpind_match(void) +{ + return &ktcpind_match; +} + +const struct modem_chat_match *hl78xx_get_ktcpcfg_match(void) +{ + return &ktcpcfg_match; +} + +const struct modem_chat_match *hl78xx_get_cgdcontrdp_match(void) +{ + return &cgdcontrdp_match; +} + +const struct modem_chat_match *hl78xx_get_ktcp_state_match(void) +{ + return &ktcp_state_match; +} + +/* modem_init_chat is implemented in hl78xx.c so it can construct the + * modem_chat_config with device-local buffer sizes (argv_size) without + * relying on ARRAY_SIZE at file scope inside this translation unit. + */ + +/* Bridge function - modem_chat callback */ +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); + } +} + +/* --- Wrapper helpers -------------------------------------------------- */ +const struct modem_chat_match *hl78xx_get_ok_match(void) +{ + return &hl78xx_ok_match; +} + +const struct modem_chat_match *hl78xx_get_abort_matches(void) +{ + return hl78xx_abort_matches; +} + +const struct modem_chat_match *hl78xx_get_unsol_matches(void) +{ + return hl78xx_unsol_matches; +} + +size_t hl78xx_get_unsol_matches_size(void) +{ + /* Return size as a runtime value to avoid constant-expression errors + * in translation units that include this header. + */ + return (size_t)(ARRAY_SIZE(hl78xx_unsol_matches)); +} + +size_t hl78xx_get_abort_matches_size(void) +{ + return (size_t)(ARRAY_SIZE(hl78xx_abort_matches)); +} + +const struct modem_chat_match *hl78xx_get_allow_match(void) +{ + return hl78xx_allow_match; +} + +size_t hl78xx_get_allow_match_size(void) +{ + return (size_t)(ARRAY_SIZE(hl78xx_allow_match)); +} + +/* Run the predefined init script for the given device */ +int hl78xx_run_init_script(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script(&data->chat, &hl78xx_init_chat_script); +} + +/* Run the periodic script */ +int hl78xx_run_periodic_script(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script(&data->chat, &hl78xx_periodic_chat_script); +} + +int hl78xx_run_init_script_async(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script_async(&data->chat, &hl78xx_init_chat_script); +} + +int hl78xx_run_periodic_script_async(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script_async(&data->chat, &hl78xx_periodic_chat_script); +} + +const struct modem_chat_match *hl78xx_get_ksrat_match(void) +{ + return &hl78xx_ksrat_match; +} + +int hl78xx_run_post_restart_script(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script(&data->chat, &hl78xx_post_restart_chat_script); +} + +int hl78xx_run_post_restart_script_async(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script_async(&data->chat, &hl78xx_post_restart_chat_script); +} + +int hl78xx_run_init_fail_script_async(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script_async(&data->chat, &init_fail_script); +} + +int hl78xx_run_enable_ksup_urc_script_async(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script_async(&data->chat, &hl78xx_enable_ksup_urc_script); +} + +int hl78xx_run_pwroff_script_async(struct hl78xx_data *data) +{ + if (!data) { + return -EINVAL; + } + return modem_chat_run_script_async(&data->chat, &hl78xx_pwroff_script); +} diff --git a/drivers/modem/hl78xx/hl78xx_chat.h b/drivers/modem/hl78xx/hl78xx_chat.h new file mode 100644 index 0000000000000..eb3a1dec8365b --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_chat.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * hl78xx_chat.h + * + * Wrapper accessors for MODEM_CHAT_* objects that live in a dedicated + * translation unit (hl78xx_chat.c). Other driver TUs should only call + * these functions instead of taking addresses or using sizeof/ARRAY_SIZE + * on the macro-generated objects. + */ +#ifndef ZEPHYR_DRIVERS_MODEM_HL78XX_HL78XX_CHAT_H_ +#define ZEPHYR_DRIVERS_MODEM_HL78XX_HL78XX_CHAT_H_ + +#include +#include + +/* Forward declare driver data type to keep this header lightweight and avoid + * circular includes. The implementation file (hl78xx_chat.c) includes + * hl78xx.h for full driver visibility. + */ +struct hl78xx_data; + +/* Chat callback bridge used by driver TUs to receive script results. */ +void hl78xx_chat_callback_handler(struct modem_chat *chat, enum modem_chat_script_result result, + void *user_data); + +/* Wrapper helpers so other translation units don't need compile-time + * visibility of the MODEM_CHAT_* macro-generated symbols. + */ +const struct modem_chat_match *hl78xx_get_ok_match(void); +const struct modem_chat_match *hl78xx_get_abort_matches(void); +const struct modem_chat_match *hl78xx_get_unsol_matches(void); +size_t hl78xx_get_unsol_matches_size(void); +size_t hl78xx_get_abort_matches_size(void); +const struct modem_chat_match *hl78xx_get_allow_match(void); +size_t hl78xx_get_allow_match_size(void); + +/* Run predefined scripts from other units */ +int hl78xx_run_init_script(struct hl78xx_data *data); +int hl78xx_run_periodic_script(struct hl78xx_data *data); +int hl78xx_run_post_restart_script(struct hl78xx_data *data); +int hl78xx_run_init_fail_script_async(struct hl78xx_data *data); +int hl78xx_run_enable_ksup_urc_script_async(struct hl78xx_data *data); +int hl78xx_run_pwroff_script_async(struct hl78xx_data *data); +int hl78xx_run_post_restart_script_async(struct hl78xx_data *data); +/* Async runners for init/periodic scripts */ +int hl78xx_run_init_script_async(struct hl78xx_data *data); +int hl78xx_run_periodic_script_async(struct hl78xx_data *data); + +/* Getter for ksrat match (moved into chat TU) */ +const struct modem_chat_match *hl78xx_get_ksrat_match(void); + +/* Socket-related chat matches used by the sockets TU */ +const struct modem_chat_match *hl78xx_get_sockets_ok_match(void); +const struct modem_chat_match *hl78xx_get_connect_matches(void); +size_t hl78xx_get_connect_matches_size(void); +const struct modem_chat_match *hl78xx_get_sockets_allow_matches(void); +size_t hl78xx_get_sockets_allow_matches_size(void); +const struct modem_chat_match *hl78xx_get_kudpind_match(void); +const struct modem_chat_match *hl78xx_get_ktcpind_match(void); +const struct modem_chat_match *hl78xx_get_ktcpcfg_match(void); +const struct modem_chat_match *hl78xx_get_cgdcontrdp_match(void); +const struct modem_chat_match *hl78xx_get_ktcp_state_match(void); + +#endif /* ZEPHYR_DRIVERS_MODEM_HL78XX_HL78XX_CHAT_H_ */ 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 0000000000000..ea00a6b9cef76 --- /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 0000000000000..e002f24ec2c1e --- /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 0000000000000..7bb2e688973ab --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.c @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#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 struct hl78xx_evt_monitor_entry *monitor_list_head; +static struct k_spinlock monitor_list_lock; + +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; +} + +/* Register an event monitor */ +int hl78xx_evt_monitor_register(struct hl78xx_evt_monitor_entry *mon) +{ + k_spinlock_key_t key = k_spin_lock(&monitor_list_lock); + + mon->next = monitor_list_head; + monitor_list_head = mon; + k_spin_unlock(&monitor_list_lock, key); + return 0; +} + +/* Unregister an event monitor */ +int hl78xx_evt_monitor_unregister(struct hl78xx_evt_monitor_entry *mon) +{ + k_spinlock_key_t key = k_spin_lock(&monitor_list_lock); + struct hl78xx_evt_monitor_entry **pp = &monitor_list_head; + + while (*pp) { + if (*pp == mon) { + *pp = mon->next; + mon->next = NULL; + k_spin_unlock(&monitor_list_lock, key); + return 0; + } + pp = &(*pp)->next; + } + + k_spin_unlock(&monitor_list_lock, key); + return -ENOENT; +} +/* 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; + /* Global monitors: SECTION_ITERABLE */ + STRUCT_SECTION_FOREACH(hl78xx_evt_monitor_entry, e) { + if (!is_paused(e)) { + if (is_direct(e)) { + LOG_DBG("calling direct global handler %p", + e->handler); + e->handler(notif, NULL); /* NULL context for global listeners */ + } else { + monitored = true; + } + } + } + + k_spinlock_key_t key = k_spin_lock(&monitor_list_lock); + + for (struct hl78xx_evt_monitor_entry *e = monitor_list_head; e; e = e->next) { + if (!is_paused(e)) { + if (is_direct(e)) { + LOG_DBG("calling direct instance handler %p " + "(ctx=%p)", + e->handler, e); + e->handler(notif, e); + } else { + monitored = true; + } + } + } + k_spin_unlock(&monitor_list_lock, key); + + 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, e); + } + } + /* Instance/context monitors */ + k_spinlock_key_t key = k_spin_lock(&monitor_list_lock); + + for (struct hl78xx_evt_monitor_entry *e = monitor_list_head; e; e = e->next) { + if (!is_paused(e) && !is_direct(e)) { + e->handler(&evt_notif->data, e); + } + } + k_spin_unlock(&monitor_list_lock, key); + + 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 0000000000000..c6c32940ba1fc --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_evt_monitor/hl78xx_evt_monitor.ld @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/*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 0000000000000..9840ea906a36b --- /dev/null +++ b/drivers/modem/hl78xx/hl78xx_sockets.c @@ -0,0 +1,2608 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) && defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS) +#include "tls_internal.h" +#include +#endif + +#include +#include +#include +#include "hl78xx.h" +#include "hl78xx_chat.h" +#include "hl78xx_cfg.h" + +LOG_MODULE_REGISTER(hl78xx_socket, CONFIG_MODEM_LOG_LEVEL); + +/* + * hl78xx_sockets.c + * + * Responsibilities: + * - Provide the socket offload integration for the HL78xx modem. + * - Parse modem URC/chat replies used to transfer payloads over the UART pipe. + * - Format and send AT commands for socket lifecycle (create, connect, send, recv, + * close, delete) and handle their confirmation/URC callbacks. + * - Provide TLS credential handling when enabled. + */ + +/* Helper macros and constants */ +#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 HL78XX_UART_PIPE_WORK_SOCKET_BUFFER_SIZE 32 +/* modem socket id is 1-based */ +#define HL78XX_TCP_STATUS_ID(x) ((x > 1 ? (x) - 1 : 0)) +/* modem socket id is 1-based */ +#define HL78XX_UDP_STATUS_ID(x) ((x > 1 ? (x) - 1 : 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); + +struct hl78xx_dns_info { +#ifdef CONFIG_NET_IPV4 + char v4_string[NET_IPV4_ADDR_LEN]; + struct in_addr v4; +#endif +#ifdef CONFIG_NET_IPV6 + char v6_string[NET_IPV6_ADDR_LEN]; + struct in6_addr v6; +#endif + bool ready; +}; + +/* IPv4 information is optional and only present when IPv4 is enabled */ +#ifdef CONFIG_NET_IPV4 +struct hl78xx_ipv4_info { + struct in_addr addr; + struct in_addr subnet; + struct in_addr gateway; + struct in_addr new_addr; +}; +#endif +/* IPv6 information is optional and only present when IPv6 is enabled */ +#ifdef CONFIG_NET_IPV6 +struct hl78xx_ipv6_info { + struct in6_addr addr; + struct in6_addr subnet; + struct in6_addr gateway; + struct in6_addr new_addr; +}; +#endif +/* TLS information is optional and only present when TLS is enabled */ +struct hl78xx_tls_info { + char hostname[MDM_MAX_HOSTNAME_LEN]; + bool hostname_set; +}; + +enum hl78xx_tcp_socket_status_code { + /** Error occurred, socket is not usable */ + TCP_SOCKET_ERROR = 0, + /** Connection is up, socket can be used to send/receive data */ + TCP_SOCKET_CONNECTED, +}; + +enum hl78xx_udp_socket_status_code { + UDP_SOCKET_ERROR = 0, /* Error occurred, socket is not usable */ + /** Connection is up, socket can be used to send/receive data */ + UDP_SOCKET_CREATED, +}; +struct hl78xx_tcp_status { + enum hl78xx_tcp_socket_status_code err_code; + bool is_connected; + bool is_created; +}; +struct hl78xx_udp_status { + enum hl78xx_udp_socket_status_code err_code; + bool is_created; +}; + +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; +}; +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; + bool socket_data_error; +#if defined(CONFIG_NET_IPV4) || defined(CONFIG_NET_IPV6) + struct hl78xx_dns_info dns; +#endif +#ifdef CONFIG_NET_IPV4 + struct hl78xx_ipv4_info ipv4; +#endif +#ifdef CONFIG_NET_IPV6 + struct hl78xx_ipv6_info ipv6; +#endif + /* rx net buffer */ + struct ring_buf *buf_pool; + uint32_t expected_buf_len; + uint32_t collected_buf_len; + struct receive_socket_data receive_buf; + /* device information */ + const struct device *modem_dev; + const struct device *offload_dev; + struct hl78xx_data *mdata_global; + /* socket state */ + struct hl78xx_tls_info tls; + struct hl78xx_tcp_status tcp_conn_status[MDM_MAX_SOCKETS]; + struct hl78xx_udp_status udp_conn_status[MDM_MAX_SOCKETS]; + /* per-socket parser state (migrated from globals) - use a small enum to + * make the parser's intent explicit and easier to read. + */ + enum { + HL78XX_PARSER_IDLE = 0, + HL78XX_PARSER_CONNECT_MATCHED, + HL78XX_PARSER_EOF_OK_MATCHED, + HL78XX_PARSER_ERROR_MATCHED, + } parser_state; + /* transient: prevents further parsing until parser_reset clears it */ + bool parser_match_found; + uint16_t parser_start_index_eof; + uint16_t parser_size_of_socketdata; + /* true once payload has been pushed into ring_buf */ + bool parser_socket_data_received; + /* set when EOF pattern was found and payload pushed */ + bool parser_eof_detected; + /* set when OK token was matched after payload */ + bool parser_ok_detected; +}; + +static struct hl78xx_socket_data *socket_data_global; + +/* ===== Utils ========================================================== + * Small, stateless utility helpers used across this file. + * Grouping here reduces cognitive load when navigating the file. + */ +static inline void hl78xx_set_socket_global(struct hl78xx_socket_data *d) +{ + socket_data_global = d; +} + +static inline struct hl78xx_socket_data *hl78xx_get_socket_global(void) +{ + return socket_data_global; +} + +/* Helper: map an internal return code into POSIX errno and set errno. + * - negative values are assumed to be negative errno semantics -> map to positive + * - positive values are assumed already POSIX errno -> pass through + * - zero or unknown -> fallback to EIO + */ +static inline void hl78xx_set_errno_from_code(int code) +{ + if (code < 0) { + errno = -code; + } else if (code > 0) { + errno = code; + } else { + errno = EIO; + } +} +/* ===== Forward declarations ========================================== + * Group commonly used static helper prototypes here so callers can be + * reordered without implicit-declaration warnings. Keep this section + * compact. When moving functions into groups, add any new prototypes + * here first. + */ +static void check_tcp_state_if_needed(struct hl78xx_socket_data *socket_data, + struct modem_socket *sock); +/* Parser helpers */ +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); +static bool parse_ip(bool is_ipv4, const char *ip_str, void *out_addr); +static bool update_dns(struct hl78xx_socket_data *socket_data, bool is_ipv4, const char *dns_str); +static void set_iface(struct hl78xx_socket_data *socket_data, bool is_ipv4); +static void parser_reset(struct hl78xx_socket_data *socket_data); +static void found_reset(struct hl78xx_socket_data *socket_data); +static bool modem_chat_parse_end_del_start(struct hl78xx_socket_data *socket_data, + struct modem_chat *chat); +static bool modem_chat_parse_end_del_complete(struct hl78xx_socket_data *socket_data, + struct modem_chat *chat); +static bool modem_chat_match_matches_received(struct hl78xx_socket_data *socket_data, + const char *match, uint16_t match_size); + +/* Receive / parser entrypoints */ +static void socket_process_bytes(struct hl78xx_socket_data *socket_data, char byte); +static int modem_process_handler(struct hl78xx_data *data); +static void modem_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event, + void *user_data); + +/* Socket I/O helpers */ +static int on_cmd_sockread_common(int socket_id, uint16_t socket_data_length, uint16_t len, + void *user_data); +static ssize_t offload_recvfrom(void *obj, void *buf, size_t len, int flags, struct sockaddr *from, + socklen_t *fromlen); +static int prepare_send_cmd(const struct modem_socket *sock, const struct sockaddr *dst_addr, + size_t buf_len, char *cmd_buf, size_t cmd_buf_size); +static int send_data_buffer(struct hl78xx_socket_data *socket_data, const char *buf, + const size_t buf_len, int *sock_written); + +/* Socket lifecycle */ +static int create_socket(struct modem_socket *sock, const struct sockaddr *addr, + struct hl78xx_socket_data *data); +static int socket_close(struct hl78xx_socket_data *socket_data, struct modem_socket *sock); +static int socket_delete(struct hl78xx_socket_data *socket_data, struct modem_socket *sock); +static void socket_notify_data(int socket_id, int new_total, void *user_data); +/* ===== TLS prototypes (conditional) ================================== + * Forward declarations for TLS-related helpers. Grouped separately so + * TLS-specific code paths are easy to find. + */ +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) && defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS) +static int map_credentials(struct hl78xx_socket_data *socket_data, const void *optval, + socklen_t optlen); +static int hl78xx_configure_chipper_suit(struct hl78xx_socket_data *socket_data); +#endif /* CONFIG_NET_SOCKETS_SOCKOPT_TLS */ + +/* ===== Container helpers ============================================= + * Small helpers used to map between container structures and their + * member pointers (eg. `modem_socket` -> `hl78xx_socket_data`). + */ +static inline struct hl78xx_socket_data *hl78xx_socket_data_from_sock(struct modem_socket *sock) +{ + /* Robustly recover the parent `hl78xx_socket_data` for any element + * address within the `sockets[]` array. Using CONTAINER_OF with + * `sockets[0]` is not safe when `sock` points to `sockets[i]` (i>0), + * because CONTAINER_OF assumes the pointer is to the member named + * in the macro (sockets[0]). That yields a pointer offset by + * i * sizeof(sockets[0]). + * + * Strategy: for each possible index i, compute the candidate parent + * base address so that &candidate->sockets[i] == sock. If the math + * yields a candidate that looks like a valid container, return it. + */ + if (!sock) { + return NULL; + } + + const size_t elem_size = sizeof(((struct hl78xx_socket_data *)0)->sockets[0]); + const size_t sockets_off = offsetof(struct hl78xx_socket_data, sockets); + struct hl78xx_socket_data *result = NULL; + + for (int i = 0; i < MDM_MAX_SOCKETS; i++) { + struct hl78xx_socket_data *candidate = + (struct hl78xx_socket_data *)((char *)sock - + (ptrdiff_t)(sockets_off + + (size_t)i * elem_size)); + /* Quick sanity: does candidate->sockets[i] point back to sock? */ + if ((struct modem_socket *)&candidate->sockets[i] != sock) { + continue; + } + if (candidate->offload_dev && candidate->mdata_global) { + return candidate; + } + + /* Remember the first match as a fallback */ + if (!result) { + result = candidate; + } + } + return result; +} + +/* ===== Chat callbacks (grouped) ===================================== + * Group all chat/URC handlers together to make the socket TU easier to + * scan. These handlers are registered via hl78xx_chat getters in + * `hl78xx_chat.c` and forward URC context into the socket layer. + */ +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], -1, "socket_id"); + new_total = ATOI(argv[2], -1, "length"); + if (socket_id < 0 || new_total < 0) { + return; + } + HL78XX_LOG_DBG("%d %d %d", __LINE__, socket_id, new_total); + /* Notify the socket layer that data is available */ + socket_notify_data(socket_id, new_total, user_data); +} + +/** +KTCP_NOTIF: , */ +void hl78xx_on_ktcpnotif(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + enum hl78xx_tcp_notif tcp_notif_received; + int socket_id = -1; + int tcp_notif = -1; + + if (!data || !socket_data) { + LOG_ERR("%s: invalid user_data", __func__); + return; + } + if (argc < 2) { + return; + } + socket_id = ATOI(argv[1], -1, "socket_id"); + tcp_notif = ATOI(argv[2], -1, "tcp_notif"); + if (tcp_notif == -1) { + return; + } + tcp_notif_received = (enum hl78xx_tcp_notif)tcp_notif; + /* Store the socket id for the notification */ + socket_data->requested_socket_id = socket_id; + switch (tcp_notif_received) { + case TCP_NOTIF_REMOTE_DISCONNECTION: + /** + * To Handle remote disconnection + * give a dummy packet size of 1 + * + */ + socket_notify_data(socket_id, 1, user_data); + break; + case TCP_NOTIF_NETWORK_ERROR: + /* Handle network error */ + break; + default: + break; + } +} + +void hl78xx_on_ktcpind(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + struct modem_socket *sock = NULL; + int socket_id = -1; + int tcp_conn_stat = -1; + + if (!data || !socket_data) { + LOG_ERR("%s: invalid user_data", __func__); + return; + } + if (argc < 3 || !argv[1] || !argv[2]) { + LOG_ERR("TCP_IND: Incomplete response"); + goto exit; + } + socket_id = ATOI(argv[1], -1, "socket_id"); + if (socket_id == -1) { + goto exit; + } + sock = modem_socket_from_id(&socket_data->socket_config, socket_id); + tcp_conn_stat = ATOI(argv[2], -1, "tcp_status"); + if (tcp_conn_stat == TCP_SOCKET_CONNECTED) { + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].err_code = + tcp_conn_stat; + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_connected = true; + return; + } +exit: + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].err_code = tcp_conn_stat; + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_connected = false; + if (socket_id != -1) { + modem_socket_put(&socket_data->socket_config, sock->sock_fd); + } +} + +/* Chat/URC handler for socket-create/indication responses + * Matches +KTCPCFG: + */ +void hl78xx_on_ktcpsocket_create(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + struct modem_socket *sock = NULL; + int socket_id = -1; + + if (!data || !socket_data) { + LOG_ERR("%s: invalid user_data", __func__); + return; + } + if (argc < 2 || !argv[1]) { + LOG_ERR("%s: Incomplete response", __func__); + goto exit; + } + /* argv[0] may contain extra CSV fields; parse leading integer */ + socket_id = ATOI(argv[1], -1, "socket_id"); + if (socket_id <= 0) { + LOG_DBG("unable to parse socket id from '%s'", argv[1]); + goto exit; + } + /* Try to find a reserved/new socket slot and assign the modem-provided id. */ + sock = modem_socket_from_newid(&socket_data->socket_config); + if (!sock) { + goto exit; + } + + if (modem_socket_id_assign(&socket_data->socket_config, sock, socket_id) < 0) { + LOG_ERR("Failed to assign modem socket id %d to fd %d", socket_id, sock->sock_fd); + goto exit; + } else { + LOG_DBG("Assigned modem socket id %d to fd %d", socket_id, sock->sock_fd); + } + + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_created = true; + return; + +exit: + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].err_code = TCP_SOCKET_ERROR; + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(socket_id)].is_created = false; + if (socket_id != -1 && sock) { + modem_socket_put(&socket_data->socket_config, sock->sock_fd); + } +} +/* Chat/URC handler for socket-create/indication responses + * Matches +KUDPCFG: + * +KUDP_IND: ,... (or +KTCP_IND) + */ +void hl78xx_on_kudpsocket_create(struct modem_chat *chat, char **argv, uint16_t argc, + void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + struct modem_socket *sock = NULL; + int socket_id = -1; + int udp_create_stat = -1; + + if (!data || !socket_data) { + LOG_ERR("%s: invalid user_data", __func__); + return; + } + if (argc < 2 || !argv[1]) { + LOG_ERR("%s: Incomplete response", __func__); + goto exit; + } + /* argv[0] may contain extra CSV fields; parse leading integer */ + socket_id = ATOI(argv[1], -1, "socket_id"); + if (socket_id <= 0) { + LOG_DBG("unable to parse socket id from '%s'", argv[1]); + goto exit; + } + /* Try to find a reserved/new socket slot and assign the modem-provided id. */ + sock = modem_socket_from_newid(&socket_data->socket_config); + if (!sock) { + goto exit; + } + + if (modem_socket_id_assign(&socket_data->socket_config, sock, socket_id) < 0) { + LOG_ERR("Failed to assign modem socket id %d to fd %d", socket_id, sock->sock_fd); + goto exit; + } else { + LOG_DBG("Assigned modem socket id %d to fd %d", socket_id, sock->sock_fd); + } + /* Parse connection status: 1=created, otherwise=error */ + udp_create_stat = ATOI(argv[2], 0, "udp_status"); + if (udp_create_stat == UDP_SOCKET_CREATED) { + socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].err_code = + udp_create_stat; + socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].is_created = true; + return; + } +exit: + socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].err_code = UDP_SOCKET_ERROR; + socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(socket_id)].is_created = false; + if (socket_id != -1 && sock) { + modem_socket_put(&socket_data->socket_config, sock->sock_fd); + } +} + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG +#ifdef CONFIG_MODEM_HL78XX_12 +/** + * @brief Handle modem state update from +KSTATE URC of RAT Scan Finish. + * This command is intended to report events for different important state transitions and system + * occurrences. + * Actually this eventc'state is really important functionality to understand networks + * searching phase of the modem. + * Verbose debug logging for KSTATEV events + */ +void hl78xx_on_kstatev_parser(struct hl78xx_data *data, int state, int rat_mode) +{ + switch (state) { + case EVENT_START_SCAN: + break; + case EVENT_FAIL_SCAN: + LOG_DBG("Modem failed to find a suitable network"); + break; + case EVENT_ENTER_CAMPED: + LOG_DBG("Modem entered camped state on a suitable or acceptable cell"); + break; + case EVENT_CONNECTION_ESTABLISHMENT: + LOG_DBG("Modem successfully established a connection to the network"); + break; + case EVENT_START_RESCAN: + LOG_DBG("Modem is starting a rescan for available networks"); + break; + case EVENT_RRC_CONNECTED: + LOG_DBG("Modem has established an RRC connection with the network"); + break; + case EVENT_NO_SUITABLE_CELLS: + LOG_DBG("Modem did not find any suitable cells during the scan"); + break; + case EVENT_ALL_REGISTRATION_FAILED: + LOG_DBG("Modem failed to register to any network"); + break; + default: + LOG_DBG("Unhandled KSTATEV for state %d", state); + break; + } +} +#endif +/** + * @brief This function doesn't handle incoming UDP data. + * It is just a placeholder for verbose debug logging of incoming UDP data. + * +KUDP_RCV: ,, + */ +void hl78xx_on_udprcv(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + if (argc < 2) { + return; + } + HL78XX_LOG_DBG("%d %d [%s] [%s] [%s]", __LINE__, argc, argv[0], argv[1], argv[2]); +} +#endif +/* Handler for +CGCONTRDP: ,,,,,,[,] + * This function is invoked by the chat layer when a CGCONTRDP URC is matched. + * It extracts the PDP context address, gateway and DNS servers and updates the + * per-instance socket_data DNS fields so dns_work_cb() can apply them. + */ +void hl78xx_on_cgdcontrdp(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) +{ + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + const char *addr_field = NULL; + const char *gw_field = NULL; + const char *dns_field = NULL; + const char *apn_field = NULL; + + /* Accept both comma-split argv[] or a single raw token that needs tokenizing */ + if (argc >= 7) { + apn_field = argv[3]; + addr_field = argv[4]; + gw_field = argv[5]; + dns_field = argv[6]; + } else { + LOG_ERR("Incomplete CGCONTRDP response: argc=%d", argc); + return; + } + + LOG_INF("Apn=%s", apn_field); + LOG_INF("Addr=%s", addr_field); + LOG_INF("Gw=%s", gw_field); + LOG_INF("DNS=%s", dns_field); +#ifdef CONFIG_MODEM_HL78XX_APN_SOURCE_NETWORK + if (apn_field) { + hl78xx_extract_essential_part_apn(apn_field, data->identity.apn, + sizeof(data->identity.apn)); + } +#endif + /* Handle address parsing: IPv4 replies sometimes embed subnet as extra + * octets concatenated after the IP (e.g. "10.149.122.90.255.255.255.252"). + * Split and parse into the instance IPv4 fields so the interface can be + * configured before the DNS resolver is invoked. + */ +#ifdef CONFIG_NET_IPV4 + if (addr_field && strchr(addr_field, '.') && !strchr(addr_field, ':')) { + char ip_addr[NET_IPV6_ADDR_LEN] = {0}; + char subnet_mask[NET_IPV6_ADDR_LEN] = {0}; + + if (!split_ipv4_and_subnet(addr_field, ip_addr, sizeof(ip_addr), subnet_mask, + sizeof(subnet_mask))) { + LOG_ERR("CGCONTRDP: failed to split IPv4+subnet: %s", addr_field); + return; + } + if (!parse_ip(true, ip_addr, &socket_data->ipv4.new_addr)) { + return; + } + if (!parse_ip(true, subnet_mask, &socket_data->ipv4.subnet)) { + return; + } + if (gw_field && !parse_ip(true, gw_field, &socket_data->ipv4.gateway)) { + return; + } + } +#else + ARG_UNUSED(gw_field); +#endif + +#ifdef CONFIG_NET_IPV6 + if (addr_field && strchr(addr_field, ':') && + !parse_ip(false, addr_field, &socket_data->ipv6.new_addr)) { + return; + } +#endif + /* Update DNS and configure interface */ + if (!update_dns(socket_data, +#ifdef CONFIG_NET_IPV4 + (addr_field && strchr(addr_field, '.') && !strchr(addr_field, ':')), +#else + false, +#endif + dns_field ? dns_field : "")) { + return; + } + /* Configure the interface addresses so net_if_is_up()/address selection + * will succeed before attempting to reconfigure the resolver. + */ +#ifdef CONFIG_NET_IPV4 + set_iface(socket_data, (addr_field && strchr(addr_field, '.') && !strchr(addr_field, ':'))); +#elif defined(CONFIG_NET_IPV6) + set_iface(socket_data, false); +#endif + + socket_data->dns.ready = false; + LOG_DBG("CGCONTRDP processed, dns strings: v4=%s v6=%s", +#ifdef CONFIG_NET_IPV4 + socket_data->dns.v4_string, +#else + "", +#endif +#ifdef CONFIG_NET_IPV6 + socket_data->dns.v6_string +#else + "" +#endif + ); +} +/* ===== Network / Parsing Utilities =================================== + * Helpers that operate on IP address parsing and DNS/address helpers. + */ +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(struct hl78xx_socket_data *socket_data, bool is_ipv4, const char *dns_str) +{ + int ret; + + /* ===== Interface helpers ============================================== + * Helpers that configure the network interface for IPv4/IPv6. + */ + LOG_DBG("Updating DNS (%s): %s", is_ipv4 ? "IPv4" : "IPv6", dns_str); +#ifdef CONFIG_NET_IPV4 + 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); + } +#else + if (is_ipv4) { + LOG_DBG("IPv4 DNS reported but IPv4 disabled in build; ignoring"); + return false; + } +#endif /* CONFIG_NET_IPV4 */ +#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 /* CONFIG_NET_IPV6 */ + return true; +} + +static void set_iface(struct hl78xx_socket_data *socket_data, bool is_ipv4) +{ + 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 (socket_data->ipv4.addr.s_addr != 0) { + net_if_ipv4_addr_rm(socket_data->net_iface, &socket_data->ipv4.addr); + } + /* Use MANUAL so the stack treats this as a configured address and it is + * available for source address selection immediately. + */ + if (!net_if_ipv4_addr_add(socket_data->net_iface, &socket_data->ipv4.new_addr, + NET_ADDR_MANUAL, 0)) { + LOG_ERR("Failed to set IPv4 interface address."); + } + + net_if_ipv4_set_netmask_by_addr(socket_data->net_iface, &socket_data->ipv4.new_addr, + &socket_data->ipv4.subnet); + net_if_ipv4_set_gw(socket_data->net_iface, &socket_data->ipv4.gateway); + + net_ipaddr_copy(&socket_data->ipv4.addr, &socket_data->ipv4.new_addr); + LOG_DBG("IPv4 interface configuration complete."); + + (void)net_if_up(socket_data->net_iface); +#else + LOG_DBG("IPv4 disabled: skipping IPv4 interface configuration"); +#endif /* CONFIG_NET_IPV4 */ + } +#ifdef CONFIG_NET_IPV6 + else { + net_if_ipv6_addr_rm(socket_data->net_iface, &socket_data->ipv6.addr); + + if (!net_if_ipv6_addr_add(socket_data->net_iface, &socket_data->ipv6.new_addr, + NET_ADDR_MANUAL, 0)) { + LOG_ERR("Failed to set IPv6 interface address."); + } else { + LOG_DBG("IPv6 interface configuration complete."); + } + /* Ensure iface up after adding address */ + (void)net_if_up(socket_data->net_iface); + } +#endif /* CONFIG_NET_IPV6 */ +} + +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; + size_t ip_len = 0; + + 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; + } + + 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; +} + +/* ===== Validation ==================================================== + * Small validation helpers used by send/recv paths. + */ +static int validate_socket(const struct modem_socket *sock, struct hl78xx_socket_data *socket_data) +{ + if (!sock) { + errno = EINVAL; + return -1; + } + + bool not_connected = (!sock->is_connected && sock->type != SOCK_DGRAM); + bool tcp_disconnected = + (sock->type == SOCK_STREAM && + !socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].is_connected); + bool udp_not_created = + (sock->type == SOCK_DGRAM && + !socket_data->udp_conn_status[HL78XX_UDP_STATUS_ID(sock->id)].is_created); + + if (not_connected || tcp_disconnected || udp_not_created) { + errno = ENOTCONN; + return -1; + } + + return 0; +} + +/* ===== Parser helpers ================================================ + * Helpers that implement the streaming parser for incoming socket payloads + * and chat end-delimiter/EOF matching logic. + */ +static void parser_reset(struct hl78xx_socket_data *socket_data) +{ + memset(&socket_data->receive_buf, 0, sizeof(socket_data->receive_buf)); + socket_data->parser_match_found = false; +} + +static void found_reset(struct hl78xx_socket_data *socket_data) +{ + if (!socket_data) { + return; + } + /* Clear all parser progress state so a new transfer can start cleanly. */ + socket_data->parser_state = HL78XX_PARSER_IDLE; + socket_data->parser_match_found = false; + socket_data->parser_socket_data_received = false; + socket_data->parser_eof_detected = false; + socket_data->parser_ok_detected = false; +} + +static bool modem_chat_parse_end_del_start(struct hl78xx_socket_data *socket_data, + struct modem_chat *chat) +{ + if (socket_data->receive_buf.len == 0) { + return false; + } + /* If the last received byte matches any of the delimiter bytes, we are + * starting the end-delimiter sequence. Use memchr to avoid an explicit + * loop and to be clearer about intent. + */ + return memchr(chat->delimiter, + socket_data->receive_buf.buf[socket_data->receive_buf.len - 1], + chat->delimiter_size) != NULL; +} + +static bool modem_chat_parse_end_del_complete(struct hl78xx_socket_data *socket_data, + struct modem_chat *chat) +{ + if (socket_data->receive_buf.len < chat->delimiter_size) { + return false; + } + + return memcmp(&socket_data->receive_buf + .buf[socket_data->receive_buf.len - chat->delimiter_size], + chat->delimiter, chat->delimiter_size) == 0; +} + +static bool modem_chat_match_matches_received(struct hl78xx_socket_data *socket_data, + const char *match, uint16_t match_size) +{ + if (socket_data->receive_buf.len < match_size) { + return false; + } + return memcmp(socket_data->receive_buf.buf, match, match_size) == 0; +} + +static bool is_receive_buffer_full(struct hl78xx_socket_data *socket_data) +{ + return socket_data->receive_buf.len >= ARRAY_SIZE(socket_data->receive_buf.buf); +} + +static void handle_expected_length_decrement(struct hl78xx_socket_data *socket_data) +{ + /* Decrement expected length if CONNECT matched and expected length > 0 */ + if (socket_data->parser_state == HL78XX_PARSER_CONNECT_MATCHED && + socket_data->expected_buf_len > 0) { + socket_data->expected_buf_len--; + } +} + +static bool is_end_delimiter_only(struct hl78xx_socket_data *socket_data) +{ + return socket_data->receive_buf.len == socket_data->mdata_global->chat.delimiter_size; +} + +static bool is_valid_eof_index(struct hl78xx_socket_data *socket_data, uint8_t size_match) +{ + socket_data->parser_start_index_eof = socket_data->receive_buf.len - size_match - 2; + return socket_data->parser_start_index_eof < ARRAY_SIZE(socket_data->receive_buf.buf); +} + +/* Handle EOF pattern: if EOF_PATTERN is found at the expected location, + * push socket payload (excluding EOF marker) into the ring buffer. + * Returns number of bytes pushed on success, 0 otherwise. + */ +static int handle_eof_pattern(struct hl78xx_socket_data *socket_data) +{ + uint8_t size_match = strlen(EOF_PATTERN); + + if (socket_data->receive_buf.len < size_match + 2) { + return 0; + } + if (!is_valid_eof_index(socket_data, size_match)) { + return 0; + } + if (strncmp(&socket_data->receive_buf.buf[socket_data->parser_start_index_eof], EOF_PATTERN, + size_match) == 0) { + int ret = ring_buf_put(socket_data->buf_pool, socket_data->receive_buf.buf, + socket_data->parser_start_index_eof); + + if (ret <= 0) { + LOG_ERR("ring_buf_put failed: %d", ret); + return 0; + } + + /* Mark that payload was successfully pushed and EOF was detected */ + socket_data->parser_socket_data_received = true; + socket_data->parser_eof_detected = true; + LOG_DBG("pushed %d bytes to ring_buf; " + "collected_buf_len(before)=%u", + ret, socket_data->collected_buf_len); + socket_data->collected_buf_len += ret; + LOG_DBG("parser_socket_data_received=1 " + "collected_buf_len(after)=%u", + socket_data->collected_buf_len); + return ret; + } + return 0; +} + +/* Helper: centralize handling when the chat end-delimiter has been fully + * received. Returns true if caller should return immediately after handling. + */ +static bool handle_delimiter_complete(struct hl78xx_socket_data *socket_data, + struct modem_chat *chat) +{ + if (!modem_chat_parse_end_del_complete(socket_data, chat)) { + return false; + } + + if (is_end_delimiter_only(socket_data)) { + parser_reset(socket_data); + return true; + } + + socket_data->parser_size_of_socketdata = socket_data->receive_buf.len; + if (socket_data->parser_state == HL78XX_PARSER_CONNECT_MATCHED && + socket_data->parser_state != HL78XX_PARSER_EOF_OK_MATCHED) { + size_t connect_len = strlen(CONNECT_STRING); + size_t connect_plus_delim = connect_len + chat->delimiter_size; + + /* Case 1: Drop the initial "CONNECT" line including its CRLF */ + if (socket_data->receive_buf.len == connect_plus_delim && + modem_chat_match_matches_received(socket_data, CONNECT_STRING, + (uint16_t)connect_len)) { + parser_reset(socket_data); + return true; + } + + /* Case 2: Try to handle EOF; only reset if EOF was actually found/pushed */ + if (handle_eof_pattern(socket_data) > 0) { + parser_reset(socket_data); + return true; + } + + /* Not the initial CONNECT+CRLF and no EOF yet -> keep accumulating */ + return false; + } + + /* For other states, treat CRLF as end-of-line and reset as before */ + parser_reset(socket_data); + return true; +} + +/* Convenience helper for matching an exact string against the receive buffer. + * This consolidates the repeated pattern of checking length and content. + */ +static inline bool modem_chat_match_exact(struct hl78xx_socket_data *socket_data, const char *match) +{ + size_t size_match = strlen(match); + + if (socket_data->receive_buf.len != size_match) { + return false; + } + return modem_chat_match_matches_received(socket_data, match, (uint16_t)size_match); +} + +static void socket_process_bytes(struct hl78xx_socket_data *socket_data, char byte) +{ + const size_t cme_size = strlen(CME_ERROR_STRING); + + if (is_receive_buffer_full(socket_data)) { + LOG_WRN("Receive buffer overrun"); + parser_reset(socket_data); + return; + } + socket_data->receive_buf.buf[socket_data->receive_buf.len++] = byte; + handle_expected_length_decrement(socket_data); + if (handle_delimiter_complete(socket_data, &socket_data->mdata_global->chat)) { + return; + } + if (modem_chat_parse_end_del_start(socket_data, &socket_data->mdata_global->chat)) { + return; + } + if (socket_data->parser_state != HL78XX_PARSER_ERROR_MATCHED && + socket_data->parser_state != HL78XX_PARSER_CONNECT_MATCHED) { + /* Exact CONNECT match: length must equal CONNECT string length */ + if (modem_chat_match_exact(socket_data, CONNECT_STRING)) { + socket_data->parser_state = HL78XX_PARSER_CONNECT_MATCHED; + LOG_DBG("CONNECT matched. Expecting %d more bytes.", + socket_data->expected_buf_len); + return; + } + /* Partial CME ERROR match: length must be at least CME string length */ + if (socket_data->receive_buf.len >= cme_size && + modem_chat_match_matches_received(socket_data, CME_ERROR_STRING, + (uint16_t)cme_size)) { + socket_data->parser_state = + HL78XX_PARSER_ERROR_MATCHED; /* 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->socket_data_error = true; + k_sem_give(&socket_data->mdata_global->script_stopped_sem_rx_int); + return; + } + } + if (socket_data->parser_state == HL78XX_PARSER_CONNECT_MATCHED && + socket_data->parser_state != HL78XX_PARSER_EOF_OK_MATCHED && + modem_chat_match_exact(socket_data, OK_STRING)) { + socket_data->parser_state = HL78XX_PARSER_EOF_OK_MATCHED; + /* Mark that OK was observed. Payload may have already been pushed by EOF handler. + */ + socket_data->parser_ok_detected = true; + LOG_DBG("OK matched. parser_ok_detected=%d parser_socket_data_received=%d " + "collected=%u", + socket_data->parser_ok_detected, socket_data->parser_socket_data_received, + socket_data->collected_buf_len); + } +} + +/* ===== Modem pipe handlers =========================================== + * Handlers and callbacks for modem pipe events (receive/transmit). + */ +static int modem_process_handler(struct hl78xx_data *data) +{ + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + char work_buf_local[HL78XX_UART_PIPE_WORK_SOCKET_BUFFER_SIZE] = {0}; + int recv_len = 0; + int work_len = 0; + /* If no more data is expected, set leftover state and return */ + if (socket_data->expected_buf_len == 0) { + LOG_DBG("No more data expected"); + atomic_set_bit(&socket_data->mdata_global->state_leftover, + MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT); + return 0; + } + + /* Use a small stack buffer for the pipe read to avoid TU-global BSS */ + work_len = MIN(sizeof(work_buf_local), socket_data->expected_buf_len); + recv_len = + modem_pipe_receive(socket_data->mdata_global->uart_pipe, work_buf_local, work_len); + if (recv_len <= 0) { + return recv_len; + } + +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_HEXDUMP_DBG(work_buf_local, recv_len, "Received bytes:"); +#endif /* CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG */ + for (int i = 0; i < recv_len; i++) { + socket_process_bytes(socket_data, work_buf_local[i]); + } + + LOG_DBG("post-process state=%d recv_len=%d recv_buf.len=%u " + "expected=%u collected=%u socket_data_received=%d", + socket_data->parser_state, recv_len, socket_data->receive_buf.len, + socket_data->expected_buf_len, socket_data->collected_buf_len, + socket_data->parser_socket_data_received); + + /* Check if we've completed reception */ + if (socket_data->parser_eof_detected && socket_data->parser_ok_detected && + socket_data->parser_socket_data_received) { + LOG_DBG("All data received: %d bytes", socket_data->parser_size_of_socketdata); + socket_data->expected_buf_len = 0; + LOG_DBG("About to give RX semaphore (eof=%d ok=%d socket_data_received=%d " + "collected=%u)", + socket_data->parser_eof_detected, socket_data->parser_ok_detected, + socket_data->parser_socket_data_received, socket_data->collected_buf_len); + k_sem_give(&socket_data->mdata_global->script_stopped_sem_rx_int); + /* Clear parser progress after the receiver has been notified */ + found_reset(socket_data); + } + return 0; +} + +static void modem_pipe_callback(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_RECEIVE_READY: + (void)modem_process_handler(data); + break; + + case MODEM_PIPE_EVENT_TRANSMIT_IDLE: + k_sem_give(&data->script_stopped_sem_tx_int); + break; + + default: + LOG_DBG("Unhandled event: %d", event); + break; + } +} + +void notif_carrier_off(const struct device *dev) +{ + struct hl78xx_data *data = dev->data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + + net_if_carrier_off(socket_data->net_iface); +} + +void notif_carrier_on(const struct device *dev) +{ + struct hl78xx_data *data = dev->data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + + net_if_carrier_on(socket_data->net_iface); +} + +void iface_status_work_cb(struct hl78xx_data *data, modem_chat_script_callback script_user_callback) +{ + + const char *cmd = "AT+CGCONTRDP=1"; + int ret = 0; + + ret = modem_dynamic_cmd_send(data, script_user_callback, cmd, strlen(cmd), + hl78xx_get_cgdcontrdp_match(), 1, false); + if (ret < 0) { + LOG_ERR("Failed to send AT+CGCONTRDP command: %d", ret); + return; + } +} + +void dns_work_cb(const struct device *dev, bool hard_reset) +{ +#if defined(CONFIG_DNS_RESOLVER) && !defined(CONFIG_DNS_SERVER_IP_ADDRESSES) + int ret; + struct hl78xx_data *data = dev->data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + 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)]; + + if (hard_reset) { + LOG_DBG("Resetting DNS resolver"); + dnsCtx = dns_resolve_get_default(); + if (!dnsCtx) { + LOG_WRN("No default DNS resolver context available; skipping " + "reconfigure"); + socket_data->dns.ready = true; + return; + } + 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); + if (!valid_address && IS_ENABLED(CONFIG_NET_IPV4)) { + /* IPv6 DNS string is not valid, replace it with IPv4 address and recheck */ +#ifdef CONFIG_NET_IPV4 + 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); +#endif + } +#elif defined(CONFIG_NET_IPV4) + valid_address = net_ipaddr_parse(socket_data->dns.v4_string, + strlen(socket_data->dns.v4_string), &temp_addr); +#else + /* No IP stack configured */ + valid_address = false; +#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) { + LOG_DBG("DNS already ready or net_iface problem %d %d %d", !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(); + ret = dns_resolve_reconfigure(dnsCtx, dns_servers_wrapped, NULL, DNS_SOURCE_MANUAL); + 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, uint16_t socket_data_length, uint16_t len, + void *user_data) +{ + struct modem_socket *sock; + struct socket_read_data *sock_data; + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + int ret = 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 (socket_data->socket_data_error && socket_data->collected_buf_len == 0) { + errno = ECONNABORTED; + return -ECONNABORTED; + } + 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; + } + 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("%d Data retrieval mismatch: expected %u, got %d", __LINE__, len, ret); + return -EAGAIN; + } +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + LOG_HEXDUMP_DBG(sock_data->recv_buf, ret, "Received Data:"); +#endif /* CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG */ + 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; + } + 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 */ + modem_socket_next_packet_size(&socket_data->socket_config, sock); + modem_socket_packet_size_update(&socket_data->socket_config, sock, -socket_data_length); + socket_data->collected_buf_len = 0; + return len; +} + +int modem_handle_data_capture(size_t target_len, struct hl78xx_data *data) +{ + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + + return on_cmd_sockread_common(socket_data->current_sock_fd, socket_data->sizeof_socket_data, + target_len, data); +} + +static int extract_ip_family_and_port(const struct sockaddr *addr, int *af, uint16_t *port) +{ +#if defined(CONFIG_NET_IPV6) + if (addr->sa_family == AF_INET6) { + *port = ntohs(net_sin6(addr)->sin6_port); + *af = MDM_HL78XX_SOCKET_AF_IPV6; + } else { +#endif /* CONFIG_NET_IPV6 */ +#if defined(CONFIG_NET_IPV4) + if (addr->sa_family == AF_INET) { + *port = ntohs(net_sin(addr)->sin_port); + *af = MDM_HL78XX_SOCKET_AF_IPV4; + } else { +#endif /* CONFIG_NET_IPV4 */ + errno = EAFNOSUPPORT; + return -1; +#if defined(CONFIG_NET_IPV4) + } +#endif /* CONFIG_NET_IPV4 */ +#if defined(CONFIG_NET_IPV6) + } +#endif /* CONFIG_NET_IPV6 */ + return 0; +} + +static int format_ip_and_setup_tls(struct hl78xx_socket_data *socket_data, + const struct sockaddr *addr, char *ip_str, size_t ip_str_len, + struct modem_socket *sock) +{ + int ret = modem_context_sprint_ip_addr(addr, ip_str, ip_str_len); + + if (ret != 0) { + LOG_ERR("Failed to format IP!"); + errno = ENOMEM; + return -1; + } + if (sock->ip_proto == IPPROTO_TCP) { + /* Determine actual length of the formatted IP string (it may be + * shorter than the provided buffer size). Copy at most + * MDM_MAX_HOSTNAME_LEN - 1 bytes and ensure NUL-termination to + * avoid writing past the hostname buffer. + */ + size_t actual_len = strnlen(ip_str, ip_str_len); + size_t copy_len = MIN(actual_len, (size_t)MDM_MAX_HOSTNAME_LEN - 1); + + if (copy_len > 0) { + memcpy(socket_data->tls.hostname, ip_str, copy_len); + } + socket_data->tls.hostname[copy_len] = '\0'; + socket_data->tls.hostname_set = false; + } + return 0; +} + +static int send_tcp_or_tls_config(struct modem_socket *sock, uint16_t dst_port, int af, int mode, + struct hl78xx_socket_data *socket_data) +{ + int ret = 0; + char cmd_buf[sizeof("AT+KTCPCFG=#,#,\"" MODEM_HL78XX_ADDRESS_FAMILY_FORMAT + "\",#####,,,,#,,#") + + MDM_MAX_HOSTNAME_LEN + NET_IPV6_ADDR_LEN]; + + snprintk(cmd_buf, sizeof(cmd_buf), "AT+KTCPCFG=1,%d,\"%s\",%u,,,,%d,%s,0", mode, + socket_data->tls.hostname, dst_port, af, mode == 3 ? "0" : ""); + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf), + hl78xx_get_ktcpcfg_match(), 1, false); + if (ret < 0 || + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].is_created == false) { + LOG_ERR("%s ret:%d", cmd_buf, ret); + modem_socket_put(&socket_data->socket_config, sock->sock_fd); + /* Map negative internal return codes to positive errno; fall back to EIO + * when the code is non-negative but the operation failed. + */ + hl78xx_set_errno_from_code(ret); + return -1; + } + return 0; +} + +static int send_udp_config(const struct sockaddr *addr, struct hl78xx_socket_data *socket_data, + struct modem_socket *sock) +{ + int ret = 0; + char cmd_buf[64]; + uint8_t display_data_urc = 0; + +#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", 0, display_data_urc, + (addr->sa_family - 1), 0); + + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf), + hl78xx_get_kudpind_match(), 1, false); + if (ret < 0) { + goto error; + } + return 0; +error: + LOG_ERR("%s ret:%d", cmd_buf, ret); + modem_socket_put(&socket_data->socket_config, sock->sock_fd); + hl78xx_set_errno_from_code(ret); + return -1; +} + +static int create_socket(struct modem_socket *sock, const struct sockaddr *addr, + struct hl78xx_socket_data *data) +{ + LOG_DBG("entry fd=%d id=%d", sock->sock_fd, sock->id); + int af; + uint16_t dst_port; + char ip_str[NET_IPV6_ADDR_LEN]; + bool is_udp; + int mode; + int ret; + /* save destination address */ + memcpy(&sock->dst, addr, sizeof(*addr)); + if (extract_ip_family_and_port(addr, &af, &dst_port) < 0) { + return -1; + } + if (format_ip_and_setup_tls(data, addr, ip_str, sizeof(ip_str), sock) < 0) { + return -1; + } + is_udp = (sock->ip_proto == IPPROTO_UDP); + if (is_udp) { + ret = send_udp_config(addr, data, sock); + LOG_DBG("send_udp_config returned %d", ret); + return ret; + } + mode = (sock->ip_proto == IPPROTO_TLS_1_2) ? 3 : 0; + /* only TCP and TLS are supported */ + if (sock->ip_proto != IPPROTO_TCP && sock->ip_proto != IPPROTO_TLS_1_2) { + LOG_ERR("Unsupported protocol: %d", sock->ip_proto); + errno = EPROTONOSUPPORT; + return -1; + } + LOG_DBG("TCP/TLS socket, calling send_tcp_or_tls_config af=%d port=%u " + "mode=%d", + af, dst_port, mode); + ret = send_tcp_or_tls_config(sock, dst_port, af, mode, data); + LOG_DBG("send_tcp_or_tls_config returned %d", ret); + return ret; +} + +static int socket_close(struct hl78xx_socket_data *socket_data, 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_dynamic_cmd_send(socket_data->mdata_global, NULL, buf, strlen(buf), + hl78xx_get_sockets_allow_matches(), + hl78xx_get_sockets_allow_matches_size(), false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + } + return ret; +} + +static int socket_delete(struct hl78xx_socket_data *socket_data, 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); + * No need to delete udp config here according to ref guide. The at UDPCLOSE + * automatically deletes the session + */ + return 0; + } + snprintk(buf, sizeof(buf), "AT+KTCPDEL=%d", sock->id); + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, buf, strlen(buf), + hl78xx_get_sockets_allow_matches(), + hl78xx_get_sockets_allow_matches_size(), false); + if (ret < 0) { + LOG_ERR("%s ret:%d", buf, ret); + } + return ret; +} + +/* ===== Socket Offload OPS ======================================== */ + +static int offload_socket(int family, int type, int proto) +{ + int ret; + /* defer modem's socket create call to bind(); use accessor and check */ + struct hl78xx_socket_data *g = hl78xx_get_socket_global(); + + HL78XX_LOG_DBG("%d %d %d %d", __LINE__, family, type, proto); + + if (!g) { + LOG_ERR("Socket global not initialized"); + errno = ENODEV; + return -1; + } + ret = modem_socket_get(&g->socket_config, family, type, proto); + if (ret < 0) { + hl78xx_set_errno_from_code(ret); + return -1; + } + errno = 0; + return ret; +} + +static int offload_close(void *obj) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + struct hl78xx_socket_data *socket_data = NULL; + + if (!sock) { + return -EINVAL; + } + /* Recover the containing instance; guard in case sock isn't from this driver */ + socket_data = hl78xx_get_socket_global(); + if (!socket_data || !socket_data->offload_dev || + socket_data->offload_dev->data != socket_data) { + LOG_WRN("parent mismatch: parent != offload_dev->data (%p != %p)", socket_data, + socket_data ? socket_data->offload_dev->data : NULL); + errno = EINVAL; + return -1; + } + /* make sure socket is allocated and assigned an id */ + if (modem_socket_id_is_assigned(&socket_data->socket_config, sock) == false) { + return 0; + } + if (validate_socket(sock, socket_data) == 0) { + socket_close(socket_data, sock); + socket_delete(socket_data, sock); + modem_socket_put(&socket_data->socket_config, sock->sock_fd); + sock->is_connected = false; + } + /* Consider here successfully socket is closed */ + return 0; +} + +static int offload_bind(void *obj, const struct sockaddr *addr, socklen_t addrlen) +{ + struct modem_socket *sock = (struct modem_socket *)obj; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + int ret = 0; + + if (!sock || !socket_data || !socket_data->offload_dev) { + errno = EINVAL; + return -1; + } + LOG_DBG("entry for socket fd=%d id=%d", ((struct modem_socket *)obj)->sock_fd, + ((struct modem_socket *)obj)->id); + /* 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 */ + ret = create_socket(sock, addr, socket_data); + LOG_DBG("create_socket returned %d", ret); + if (ret < 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; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + int ret = 0; + char cmd_buf[sizeof("AT+KTCPCFG=#\r")]; + char ip_str[NET_IPV6_ADDR_LEN]; + + if (!addr || !socket_data || !socket_data->offload_dev) { + errno = EINVAL; + return -1; + } + if (!hl78xx_is_registered(socket_data->mdata_global)) { + errno = ENETUNREACH; + 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) { + LOG_DBG("%d no socket assigned", __LINE__); + if (create_socket(sock, addr, socket_data) < 0) { + return -1; + } + } + memcpy(&sock->dst, addr, sizeof(*addr)); + /* 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) { + hl78xx_set_errno_from_code(ret); + LOG_ERR("Error formatting IP string %d", ret); + return -1; + } + /* send connect command */ + snprintk(cmd_buf, sizeof(cmd_buf), "AT+KTCPCNX=%d", sock->id); + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf), + hl78xx_get_ktcpind_match(), 1, false); + if (ret < 0 || + socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].is_connected == false) { + sock->is_connected = false; + LOG_ERR("%s ret:%d", cmd_buf, ret); + /* Map tcp_conn_status.err_code: + * - positive values are assumed to be direct POSIX errno values -> pass + * through + * - zero or unknown -> use conservative EIO + */ + errno = (socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)].err_code > 0) + ? socket_data->tcp_conn_status[HL78XX_TCP_STATUS_ID(sock->id)] + .err_code + : EIO; + 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 wait_for_data_if_needed(struct hl78xx_socket_data *socket_data, + 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 (validate_socket(sock, socket_data) == -1) { + 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(struct hl78xx_socket_data *socket_data, 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); +} + +/* Perform the receive transaction: release chat, attach pipe, wait for tx sem, + * transmit read command, wait for rx sem and capture data. Returns 0 on + * success or a negative code which will be mapped by caller. + */ +static int hl78xx_perform_receive_transaction(struct hl78xx_socket_data *socket_data, + const char *sendbuf) +{ + int rv; + int ret; + + modem_chat_release(&socket_data->mdata_global->chat); + modem_pipe_attach(socket_data->mdata_global->uart_pipe, modem_pipe_callback, + socket_data->mdata_global); + + rv = k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER); + if (rv < 0) { + LOG_ERR("%s: k_sem_take(tx) returned %d", __func__, rv); + return rv; + } + + ret = modem_pipe_transmit(socket_data->mdata_global->uart_pipe, (const uint8_t *)sendbuf, + strlen(sendbuf)); + if (ret < 0) { + LOG_ERR("Error sending read command: %d", ret); + return ret; + } + rv = k_sem_take(&socket_data->mdata_global->script_stopped_sem_rx_int, K_FOREVER); + if (rv < 0) { + return rv; + } + + rv = modem_handle_data_capture(socket_data->sizeof_socket_data, socket_data->mdata_global); + if (rv < 0) { + return rv; + } + + return 0; +} + +static void setup_socket_data(struct hl78xx_socket_data *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; + socket_data->collected_buf_len = 0; + socket_data->socket_data_error = false; +} + +static void check_tcp_state_if_needed(struct hl78xx_socket_data *socket_data, + struct modem_socket *sock) +{ + const char *check_ktcp_stat = "AT+KTCPSTAT"; + /* Only check for TCP sockets */ + if (sock->type != SOCK_STREAM) { + return; + } + if (atomic_test_and_clear_bit(&socket_data->mdata_global->state_leftover, + MODEM_SOCKET_DATA_LEFTOVER_STATE_BIT) && + sock && sock->ip_proto == IPPROTO_TCP) { + modem_dynamic_cmd_send(socket_data->mdata_global, NULL, check_ktcp_stat, + strlen(check_ktcp_stat), hl78xx_get_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; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + char sendbuf[sizeof("AT+KUDPRCV=#,##########\r\n")]; + struct socket_read_data sock_data; + int next_packet_size = 0; + uint32_t max_data_length = 0; + uint16_t read_size = 0; + int trv = 0; + int ret; + + if (!sock || !socket_data || !socket_data->offload_dev) { + errno = EINVAL; + return -1; + } + /* If modem is not registered yet, propagate EAGAIN to indicate try again + * later. However, if the socket simply isn't connected (validate_socket + * returns -1) we return 0 with errno cleared so upper layers (eg. DNS + * dispatcher) treat this as no data available rather than an error and + * avoid noisy repeated error logs. + */ + if (!hl78xx_is_registered(socket_data->mdata_global)) { + errno = EAGAIN; + return -1; + } + if (validate_socket(sock, socket_data) == -1) { + errno = 0; + return 0; + } + + if (!validate_recv_args(buf, len, flags)) { + return -1; + } + ret = k_mutex_lock(&socket_data->mdata_global->tx_lock, K_SECONDS(1)); + if (ret < 0) { + LOG_ERR("Failed to acquire TX lock: %d", ret); + hl78xx_set_errno_from_code(ret); + return -1; + } + next_packet_size = wait_for_data_if_needed(socket_data, sock, flags); + if (next_packet_size <= 0) { + ret = next_packet_size; + goto exit; + } + max_data_length = + MDM_MAX_DATA_LENGTH - (socket_data->mdata_global->buffers.eof_pattern_size + + sizeof(MODEM_STREAM_STARTER_WORD) - 1); + /* limit read size to modem max data length */ + next_packet_size = MIN(next_packet_size, max_data_length); + /* limit read size to user buffer length */ + read_size = MIN(next_packet_size, len); + /* prepare socket data for the read operation */ + setup_socket_data(socket_data, sock, &sock_data, buf, len, from, read_size); + prepare_read_command(socket_data, sendbuf, sizeof(sendbuf), sock, read_size); + HL78XX_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"); + trv = hl78xx_perform_receive_transaction(socket_data, sendbuf); + if (trv < 0) { + hl78xx_set_errno_from_code(trv); + 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: + k_mutex_unlock(&socket_data->mdata_global->tx_lock); + modem_chat_attach(&socket_data->mdata_global->chat, socket_data->mdata_global->uart_pipe); + socket_data->expected_buf_len = 0; + check_tcp_state_if_needed(socket_data, sock); + return ret; +} + +int check_if_any_socket_connected(const struct device *dev) +{ + struct hl78xx_data *data = dev->data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + 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 */ + k_sem_give(&cfg->sem_lock); + return true; + } + } + k_sem_give(&cfg->sem_lock); + return false; +} + +/* ===== Send / Receive helpers ======================================== + * Helpers used by sendto/recv paths, preparing commands and transmitting + * data over the modem pipe. + */ +static int prepare_send_cmd(const struct modem_socket *sock, const struct sockaddr *dst_addr, + size_t buf_len, char *cmd_buf, size_t cmd_buf_size) +{ + int ret = 0; + + if (sock->ip_proto == IPPROTO_UDP) { + char ip_str[NET_IPV6_ADDR_LEN]; + uint16_t dst_port = 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; + } + + /* Default to TCP-style send command */ + snprintk(cmd_buf, cmd_buf_size, "AT+KTCPSND=%d,%zu", sock->id, buf_len); + return 0; +} + +static int send_data_buffer(struct hl78xx_socket_data *socket_data, 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 No data to send", __LINE__); + return 0; + } + while (len > 0) { + LOG_DBG("waiting for TX semaphore (offset=%u len=%d)", offset, len); + if (k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER) < + 0) { + LOG_ERR("%s: k_sem_take(tx) failed", __func__); + 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; +} + +static int validate_and_prepare(struct modem_socket *sock, const struct sockaddr **dst_addr, + size_t *buf_len, char *cmd_buf, size_t cmd_buf_len) +{ + /* Validate args and prepare send command */ + if (!sock) { + errno = EINVAL; + return -1; + } + if (sock->type != SOCK_DGRAM && !sock->is_connected) { + errno = ENOTCONN; + return -1; + } + 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; + } + /* Consolidated send command helper handles UDP vs TCP formatting */ + return prepare_send_cmd(sock, *dst_addr, *buf_len, cmd_buf, cmd_buf_len); +} + +static int transmit_regular_data(struct hl78xx_socket_data *socket_data, const char *buf, + size_t buf_len, int *sock_written) +{ + int ret; + + ret = send_data_buffer(socket_data, buf, buf_len, sock_written); + if (ret < 0) { + return ret; + } + ret = k_sem_take(&socket_data->mdata_global->script_stopped_sem_tx_int, K_FOREVER); + if (ret < 0) { + LOG_ERR("%s: k_sem_take(tx) returned %d", __func__, ret); + return ret; + } + return 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); +} + +/* send binary data via the +KUDPSND/+KTCPSND commands */ +static ssize_t send_socket_data(void *obj, struct hl78xx_socket_data *socket_data, + 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_and_prepare(sock, &dst_addr, &buf_len, cmd_buf, sizeof(cmd_buf)); + if (ret < 0) { + return ret; + } + socket_data->socket_data_error = false; + if (k_mutex_lock(&socket_data->mdata_global->tx_lock, K_SECONDS(1)) < 0) { + return -1; + } + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, cmd_buf, strlen(cmd_buf), + (const struct modem_chat_match *)hl78xx_get_connect_matches(), + hl78xx_get_connect_matches_size(), false); + if (ret < 0 || socket_data->socket_data_error) { + hl78xx_set_errno_from_code(ret); + ret = -1; + goto cleanup; + } + modem_pipe_attach(socket_data->mdata_global->chat.pipe, modem_pipe_callback, + socket_data->mdata_global); + ret = transmit_regular_data(socket_data, buf, buf_len, &sock_written); + if (ret < 0) { + goto cleanup; + } + modem_chat_attach(&socket_data->mdata_global->chat, socket_data->mdata_global->uart_pipe); + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, "", 0, + hl78xx_get_sockets_ok_match(), 1, false); + if (ret < 0) { + LOG_ERR("Final confirmation failed: %d", ret); + goto cleanup; + } +cleanup: + k_mutex_unlock(&socket_data->mdata_global->tx_lock); + return (ret < 0) ? -1 : sock_written; +} + +#ifdef CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS +/* ===== TLS implementation (conditional) ================================ + * TLS credential upload and chipper settings helper implementations. + */ +static int handle_tls_sockopts(void *obj, int optname, const void *optval, socklen_t optlen) +{ + int ret; + struct modem_socket *sock = (struct modem_socket *)obj; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + + if (!sock || !socket_data || !socket_data->offload_dev) { + return -EINVAL; + } + + switch (optname) { + case TLS_SEC_TAG_LIST: + ret = map_credentials(socket_data, optval, optlen); + return ret; + + case TLS_HOSTNAME: + if (optlen >= MDM_MAX_HOSTNAME_LEN) { + return -EINVAL; + } + memset(socket_data->tls.hostname, 0, MDM_MAX_HOSTNAME_LEN); + memcpy(socket_data->tls.hostname, optval, optlen); + socket_data->tls.hostname[optlen] = '\0'; + socket_data->tls.hostname_set = true; + ret = hl78xx_configure_chipper_suit(socket_data); + if (ret < 0) { + LOG_ERR("Failed to configure chipper suit: %d", ret); + return ret; + } + LOG_DBG("TLS hostname set to: %s", socket_data->tls.hostname); + return 0; + + case TLS_PEER_VERIFY: + if (*(const uint32_t *)optval != TLS_PEER_VERIFY_REQUIRED) { + LOG_WRN("Disabling peer verification is not supported"); + } + return 0; + + case TLS_CERT_NOCOPY: + return 0; /* No-op, success */ + + default: + LOG_DBG("Unsupported TLS option: %d", optname); + return -EINVAL; + } +} + +static int offload_setsockopt(void *obj, int level, int optname, const void *optval, + socklen_t optlen) +{ + int ret = 0; + + if (!IS_ENABLED(CONFIG_NET_SOCKETS_SOCKOPT_TLS)) { + return -EINVAL; + } + if (level == SOL_TLS) { + ret = handle_tls_sockopts(obj, optname, optval, optlen); + if (ret < 0) { + hl78xx_set_errno_from_code(ret); + return -1; + } + return 0; + } + LOG_DBG("Unsupported socket option: %d", optname); + return -EINVAL; +} +#endif /* CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS */ + +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; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + + if (!sock || !socket_data || !socket_data->offload_dev) { + errno = EINVAL; + return -1; + } + 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; + } + /* For stream sockets (TCP) the socket must be connected. For datagram + * sockets (UDP) sendto can be used without a prior connect as long as a + * destination address is provided or the socket has a stored dst. The + * helper validate_and_prepare will supply sock->dst for UDP when needed. + */ + if (sock->type != SOCK_DGRAM && !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, socket_data, to, buf, len, K_SECONDS(MDM_CMD_TIMEOUT)); + if (ret < 0) { + /* Map internal negative return codes to positive errno values. Use EIO as + * a conservative fallback when ret is non-negative (unexpected) + */ + hl78xx_set_errno_from_code(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; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + struct zsock_pollfd *pfd; + struct k_poll_event **pev; + struct k_poll_event *pev_end; + /* sanity check: does parent == parent->offload_dev->data ? */ + if (socket_data && socket_data->offload_dev && + socket_data->offload_dev->data != socket_data) { + LOG_WRN("parent mismatch: parent != offload_dev->data (%p != %p)", socket_data, + socket_data->offload_dev->data); + } + switch (request) { + case ZFD_IOCTL_POLL_PREPARE: + pfd = va_arg(args, struct zsock_pollfd *); + pev = va_arg(args, struct k_poll_event **); + pev_end = va_arg(args, struct k_poll_event *); + 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: + pfd = va_arg(args, struct zsock_pollfd *); + pev = va_arg(args, struct k_poll_event **); + return modem_socket_poll_update(obj, pfd, pev); + + case F_GETFL: + return 0; + + case F_SETFL: { +#ifdef CONFIG_MODEM_HL78XX_LOG_CONTEXT_VERBOSE_DEBUG + int flags = va_arg(args, int); + + LOG_DBG("F_SETFL called with flags=0x%x", flags); + ARG_UNUSED(flags); +#endif + /* 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}; + struct modem_socket *sock = (struct modem_socket *)obj; + struct hl78xx_socket_data *socket_data = hl78xx_socket_data_from_sock(sock); + size_t full_len = 0; + int ret; + + if (!sock || !socket_data || !socket_data->offload_dev) { + errno = EINVAL; + return -1; + } + /* 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; + } + 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++; + } + /* send_socket_data expects a buffer pointer and its byte length. + * crafted_msg.msg_iovlen is the number of iovec entries and is + * incorrect here (was causing sends of '2' bytes when two iovecs + * were present). Use the first iovec's iov_len for the byte length. + */ + ret = send_socket_data(obj, socket_data, crafted_msg.msg_name, + crafted_msg.msg_iov->iov_base, crafted_msg.msg_iov->iov_len, + K_SECONDS(MDM_CMD_TIMEOUT)); + if (bkp_iovec_idx != -1) { + msg->msg_iov[bkp_iovec_idx] = bkp_iovec; + } + if (ret < 0) { + /* Map negative internal return code to positive errno; fall back to + * EIO + */ + hl78xx_set_errno_from_code(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, +#if defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS) + .setsockopt = offload_setsockopt, +#else + .setsockopt = NULL, +#endif +}; +/* clang-format on */ +static int hl78xx_init_sockets(const struct device *dev) +{ + int ret; + struct hl78xx_socket_data *socket_data = (struct hl78xx_socket_data *)dev->data; + + socket_data->buf_pool = &mdm_recv_pool; + /* 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; +} +static void socket_notify_data(int socket_id, int new_total, void *user_data) +{ + int ret = 0; + struct modem_socket *sock; + struct hl78xx_data *data = (struct hl78xx_data *)user_data; + struct hl78xx_socket_data *socket_data = + (struct hl78xx_socket_data *)data->offload_dev->data; + + if (!data || !socket_data) { + LOG_ERR("%s: invalid user_data", __func__); + return; + } + sock = modem_socket_from_id(&socket_data->socket_config, socket_id); + if (!sock) { + return; + } + /* Update the packet size */ + 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); + } + /* Duplicate/chat callback block removed; grouped versions live earlier */ +} + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) && defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS) +static int hl78xx_configure_chipper_suit(struct hl78xx_socket_data *socket_data) +{ + const char *cmd_chipper_suit = "AT+KSSLCRYPTO=0,8,1,8192,4,4,3,0"; + + return modem_dynamic_cmd_send( + socket_data->mdata_global, NULL, cmd_chipper_suit, strlen(cmd_chipper_suit), + (const struct modem_chat_match *)hl78xx_get_ok_match(), 1, false); +} +/* send binary data via the K....STORE commands */ +static ssize_t hl78xx_send_cert(struct hl78xx_socket_data *socket_data, const char *cert_data, + size_t cert_len, enum tls_credential_type cert_type) +{ + int ret; + char send_buf[sizeof("AT+KPRIVKSTORE=#,####\r\n")]; + int sock_written = 0; + + if (!socket_data || !socket_data->mdata_global) { + return -EINVAL; + } + + if (cert_len == 0 || !cert_data) { + LOG_ERR("Invalid certificate data or length"); + return -EINVAL; + } + /** Certificate length exceeds maximum allowed size */ + if (cert_len > MDM_MAX_CERT_LENGTH) { + return -EINVAL; + } + + if (cert_type == TLS_CREDENTIAL_CA_CERTIFICATE || + cert_type == TLS_CREDENTIAL_SERVER_CERTIFICATE) { + snprintk(send_buf, sizeof(send_buf), "AT+KCERTSTORE=%d,%d", (cert_type - 1), + cert_len); + + } else if (cert_type == TLS_CREDENTIAL_PRIVATE_KEY) { + snprintk(send_buf, sizeof(send_buf), "AT+KPRIVKSTORE=0,%d", cert_len); + + } else { + LOG_ERR("Unsupported certificate type: %d", cert_type); + return -EINVAL; + } + socket_data->socket_data_error = false; + if (k_mutex_lock(&socket_data->mdata_global->tx_lock, K_SECONDS(1)) < 0) { + errno = EBUSY; + return -1; + } + ret = modem_dynamic_cmd_send(socket_data->mdata_global, NULL, send_buf, strlen(send_buf), + (const struct modem_chat_match *)hl78xx_get_connect_matches(), + hl78xx_get_connect_matches_size(), 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); + ret = send_data_buffer(socket_data, cert_data, cert_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_dynamic_cmd_send(socket_data->mdata_global, NULL, "", 0, + (const struct modem_chat_match *)hl78xx_get_ok_match(), 1, + false); + if (ret < 0) { + LOG_ERR("Final confirmation failed: %d", ret); + goto cleanup; + } +cleanup: + k_mutex_unlock(&socket_data->mdata_global->tx_lock); + return (ret < 0) ? -1 : sock_written; +} + +static int map_credentials(struct hl78xx_socket_data *socket_data, const void *optval, + socklen_t optlen) +{ + const sec_tag_t *sec_tags = (const sec_tag_t *)optval; + int ret = 0; + int tags_len; + sec_tag_t tag; + int i; + struct tls_credential *cert; + + if ((optlen % sizeof(sec_tag_t)) != 0 || (optlen == 0)) { + return -EINVAL; + } + tags_len = optlen / sizeof(sec_tag_t); + /* For each tag, retrieve the credentials value and type: */ + for (i = 0; i < tags_len; i++) { + tag = sec_tags[i]; + cert = credential_next_get(tag, NULL); + while (cert != NULL) { + switch (cert->type) { + case TLS_CREDENTIAL_CA_CERTIFICATE: + LOG_DBG("TLS_CREDENTIAL_CA_CERTIFICATE tag: %d", tag); + break; + + case TLS_CREDENTIAL_SERVER_CERTIFICATE: + LOG_DBG("TLS_CREDENTIAL_SERVER_CERTIFICATE tag: %d", tag); + break; + + case TLS_CREDENTIAL_PRIVATE_KEY: + LOG_DBG("TLS_CREDENTIAL_PRIVATE_KEY tag: %d", tag); + break; + + case TLS_CREDENTIAL_NONE: + case TLS_CREDENTIAL_PSK: + case TLS_CREDENTIAL_PSK_ID: + default: + /* Not handled */ + return -EINVAL; + } + ret = hl78xx_send_cert(socket_data, cert->buf, cert->len, cert->type); + if (ret < 0) { + return ret; + } + cert = credential_next_get(tag, cert); + } + } + + return 0; +} +#endif + +static int hl78xx_socket_init(const struct device *dev) +{ + + struct hl78xx_socket_data *data = (struct hl78xx_socket_data *)dev->data; + + data->offload_dev = dev; + /* Ensure the parent modem device pointer was set at static init time */ + if (data->modem_dev == NULL) { + LOG_ERR("modem_dev not initialized for %s", dev->name); + return -EINVAL; + } + /* Ensure the modem device is ready before accessing its driver data */ + if (!device_is_ready(data->modem_dev)) { + LOG_ERR("modem device %s not ready", data->modem_dev->name); + return -ENODEV; + } + if (data->modem_dev->data == NULL) { + LOG_ERR("modem device %s has no driver data yet", data->modem_dev->name); + return -EAGAIN; + } + data->mdata_global = (struct hl78xx_data *)data->modem_dev->data; + data->mdata_global->offload_dev = dev; + /* Keep original single global pointer usage but set via accessor. */ + hl78xx_set_socket_global(data); + atomic_set(&data->mdata_global->state_leftover, 0); + + return 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; + + /* startup trace */ + if (!data->mdata_global) { + LOG_WRN("mdata_global not set for net iface init on %s", dev->name); + } + net_if_set_link_addr( + iface, + modem_get_mac(data->mac_addr, + data->mdata_global ? data->mdata_global->identity.imei : NULL), + 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) +{ + bool fam_ok = false; + +#ifdef CONFIG_NET_IPV4 + if (family == AF_INET) { + fam_ok = true; + } +#endif +#ifdef CONFIG_NET_IPV6 + if (family == AF_INET6) { + fam_ok = true; + } +#endif + if (!fam_ok) { + return false; + } + if (!(type == SOCK_DGRAM || type == SOCK_STREAM)) { + return false; + } + if (proto == IPPROTO_TCP || proto == IPPROTO_UDP) { + return true; + } +#if defined(CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS) + if (proto == IPPROTO_TLS_1_2) { + return true; + } +#endif + return false; +} + +#define MODEM_HL78XX_DEFINE_OFFLOAD_INSTANCE(inst) \ + static struct hl78xx_socket_data hl78xx_socket_data_##inst = { \ + .modem_dev = DEVICE_DT_GET(DT_PARENT(DT_DRV_INST(inst))), \ + }; \ + NET_DEVICE_OFFLOAD_INIT( \ + inst, "hl78xx_dev", hl78xx_socket_init, NULL, &hl78xx_socket_data_##inst, 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_OFFLOAD_DEVICE_SWIR_HL78XX(inst) MODEM_HL78XX_DEFINE_OFFLOAD_INSTANCE(inst) + +#define DT_DRV_COMPAT swir_hl7812_offload +DT_INST_FOREACH_STATUS_OKAY(MODEM_OFFLOAD_DEVICE_SWIR_HL78XX) +#undef DT_DRV_COMPAT + +#define DT_DRV_COMPAT swir_hl7800_offload +DT_INST_FOREACH_STATUS_OKAY(MODEM_OFFLOAD_DEVICE_SWIR_HL78XX) +#undef DT_DRV_COMPAT diff --git a/include/zephyr/drivers/modem/hl78xx_apis.h b/include/zephyr/drivers/modem/hl78xx_apis.h new file mode 100644 index 0000000000000..bffd35d0e0c7c --- /dev/null +++ b/include/zephyr/drivers/modem/hl78xx_apis.h @@ -0,0 +1,458 @@ +/* + * 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 +#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 * (v))) +#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 +#define MDM_MANUFACTURER_LENGTH 20 +#define MDM_MODEL_LENGTH 32 +#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 MDM_MAX_CERT_LENGTH 4096 +#define MDM_MAX_HOSTNAME_LEN 128 +/** + * @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 STRUCT_SECTION_ITERABLE(hl78xx_evt_monitor_entry, name) = { \ + .handler = _handler, \ + .next = NULL, \ + .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_HL78XX_12 + HL78XX_RAT_GSM, +#ifdef CONFIG_MODEM_HL78XX_12_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 +}; + +/** Phone functionality modes */ +enum hl78xx_phone_functionality { + HL78XX_SIM_POWER_OFF, + HL78XX_FULLY_FUNCTIONAL, + HL78XX_AIRPLANE = 4, +}; +/** Module status codes */ +enum hl78xx_module_status { + HL78XX_MODULE_READY = 0, + HL78XX_MODULE_WAITING_FOR_ACCESS_CODE, + HL78XX_MODULE_SIM_NOT_PRESENT, + HL78XX_MODULE_SIMLOCK, + HL78XX_MODULE_UNRECOVERABLE_ERROR, + HL78XX_MODULE_UNKNOWN_STATE, + HL78XX_MODULE_INACTIVE_SIM +}; + +/** Cellular modem info type */ +enum hl78xx_modem_info_type { + /* Access Point Name */ + HL78XX_MODEM_INFO_APN, + /* */ + HL78XX_MODEM_INFO_CURRENT_RAT, + /* */ + HL78XX_MODEM_INFO_NETWORK_OPERATOR, +}; + +/** 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; +}; + +enum hl78xx_evt_type { + HL78XX_LTE_RAT_UPDATE, + HL78XX_LTE_REGISTRATION_STAT_UPDATE, + HL78XX_LTE_SIM_REGISTRATION, + HL78XX_LTE_MODEM_STARTUP, +}; + +struct hl78xx_evt { + enum hl78xx_evt_type type; + + union { + enum cellular_registration_status reg_status; + enum hl78xx_cell_rat_mode rat_mode; + bool status; + int value; + } 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 cellular_signal_type type, + int16_t *value); + +/** API for getting modem information */ +typedef int (*hl78xx_api_get_modem_info)(const struct device *dev, + const enum cellular_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 cellular_access_technology tech, + enum cellular_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); + +/**< Event monitor entry */ +struct hl78xx_evt_monitor_entry; /* forward declaration */ +/* Event monitor dispatcher */ +typedef void (*hl78xx_evt_monitor_dispatcher_t)(struct hl78xx_evt *notif); +/* Event monitor handler */ +typedef void (*hl78xx_evt_monitor_handler_t)(struct hl78xx_evt *notif, + struct hl78xx_evt_monitor_entry *mon); + +struct hl78xx_evt_monitor_entry { + /** Monitor callback. */ + const hl78xx_evt_monitor_handler_t handler; + /* link for runtime list */ + struct hl78xx_evt_monitor_entry *next; + struct { + uint8_t paused: 1; /* Monitor is paused. */ + uint8_t direct: 1; /* Dispatch in ISR. */ + } flags; +}; +/** + * @brief hl78xx_api_func_set_phone_functionality + * @param dev Cellular network device instance + * @param functionality phone functionality mode to set + * @param reset If true, the modem will be reset as part of applying the functionality change. + * @return 0 if successful. + */ +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 + * @param dev Cellular network device instance + * @param functionality Pointer to store the current phone functionality mode + * @return 0 if successful. + */ +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 Cellular network device instance + * @param type Type of the signal to retrieve + * @param value Pointer to store the signal value + * @return 0 if successful. + */ +int hl78xx_api_func_get_signal(const struct device *dev, const enum cellular_signal_type type, + int16_t *value); +/** + * @brief hl78xx_api_func_get_modem_info_vendor - Brief description of the function. + * @param dev Cellular network device instance + * @param type Type of the modem info to retrieve + * @param info Pointer to store the modem info + * @param size Size of the info buffer + * @return 0 if successful. + */ +int hl78xx_api_func_get_modem_info_vendor(const struct device *dev, + enum hl78xx_modem_info_type type, void *info, + size_t size); +/** + * @brief hl78xx_api_func_modem_dynamic_cmd_send - Brief description of the function. + * @param dev Cellular network device instance + * @param cmd AT command to send + * @param cmd_size Size of the AT command + * @param response_matches Expected response patterns + * @param matches_size Size of the response patterns + * @return 0 if successful. + */ +int hl78xx_api_func_modem_dynamic_cmd_send(const struct device *dev, const char *cmd, + uint16_t cmd_size, + const struct modem_chat_match *response_matches, + uint16_t matches_size); +/** + * @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, void *info, + size_t size) +{ + return hl78xx_api_func_get_modem_info_vendor(dev, type, info, 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) +{ + return hl78xx_api_func_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) +{ + return hl78xx_api_func_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) +{ + + return hl78xx_api_func_modem_dynamic_cmd_send(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_dispatcher_t handler); +/** + * @brief Register an event monitor to receive HL78xx modem event notifications. + */ +int hl78xx_evt_monitor_register(struct hl78xx_evt_monitor_entry *mon); +/** + * @brief Unregister an event monitor from receiving HL78xx modem event notifications. + */ +int hl78xx_evt_monitor_unregister(struct hl78xx_evt_monitor_entry *mon); +/** + * @brief Convert HL78xx RAT mode to standard cellular API. + */ +enum cellular_access_technology hl78xx_rat_to_access_tech(enum hl78xx_cell_rat_mode rat_mode); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_DRIVERS_HL78XX_APIS_H_ */ From 561d3baee2359d563c96c7c39378e042000a81d2 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 | 11 + .../drivers/modem/hello_hl78xx/CMakeLists.txt | 13 + samples/drivers/modem/hello_hl78xx/Kconfig | 7 + samples/drivers/modem/hello_hl78xx/README.rst | 72 ++++ .../boards/nrf9160dk_nrf9160_ns.conf | 23 ++ .../boards/nrf9160dk_nrf9160_ns.overlay | 45 +++ .../overlay-swir_hl78xx-verbose-logging.conf | 8 + samples/drivers/modem/hello_hl78xx/prj.conf | 76 ++++ .../drivers/modem/hello_hl78xx/sample.yaml | 16 + samples/drivers/modem/hello_hl78xx/src/main.c | 343 ++++++++++++++++++ samples/drivers/modem/index.rst | 5 + 11 files changed, 619 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/boards/nrf9160dk_nrf9160_ns.conf create mode 100644 samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.overlay create mode 100644 samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-verbose-logging.conf 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 0000000000000..d422a2f743fda --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/.gitignore @@ -0,0 +1,11 @@ + +# Copyright (c) 2025 Netfeasa Ltd. +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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 0000000000000..829d4bc26efe9 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/CMakeLists.txt @@ -0,0 +1,13 @@ +# Sierra Wireless HL78XX Sample CMake file + +# 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 0000000000000..decc7199c7cc6 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/Kconfig @@ -0,0 +1,7 @@ +# Sierra Wireless HL78XX Sample 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 0000000000000..55b4ea290f47d --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/README.rst @@ -0,0 +1,72 @@ +.. 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 Wireless 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 + + [00:00:12.840,000] hl78xx_socket: Apn="netfeasavodiot.mnc028.mcc901.gprs" + [00:00:12.840,000] hl78xx_socket: Addr=10.149.105.74.255.255.255.252 + [00:00:12.840,000] hl78xx_socket: Gw=10.149.105.73 + [00:00:12.840,000] hl78xx_socket: DNS=141.1.1.1 + [00:00:12.840,000] hl78xx_socket: Extracted IP: 10.149.105.74 + [00:00:12.840,000] hl78xx_socket: Extracted Subnet: 255.255.255.252 + [00:00:12.840,000] hl78xx_dev: switch from run enable gprs script to carrier on + [00:00:15.944,000] main: IP Up + [00:00:15.944,000] main: Connected to network + + ********************************************************** + ********* Hello HL78XX Modem Sample Application ********** + ********************************************************** + [00:00:15.980,000] main: Manufacturer: Sierra Wireless + [00:00:15.980,000] main: Firmware Version: HL7812.5.7.3.0 + [00:00:15.980,000] main: APN: netfeasavodiot + [00:00:15.980,000] main: Imei: 351144441214500 + [00:00:15.980,000] main: RAT: NB1 + [00:00:15.980,000] main: Connection status: Not Registered + [00:00:15.980,000] main: RSRP : -97 + ********************************************************** + + [00:00:15.980,000] main: Setting new APN: + [00:00:15.980,000] main: IP down + [00:00:15.980,000] main: Disconnected from network + [00:00:16.013,000] main: New APN: "" + [00:00:16.013,000] main: Test endpoint: flake.legato.io:6000 + [00:00:17.114,000] main: Resolved: 20.29.223.5:6000 + [00:00:17.114,000] main: Sample application finished. + +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/boards/nrf9160dk_nrf9160_ns.conf b/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.conf new file mode 100644 index 0000000000000..e2cbd919f4b86 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.conf @@ -0,0 +1,23 @@ +CONFIG_UART_ASYNC_API=y +CONFIG_UART_1_ASYNC=y +CONFIG_UART_1_INTERRUPT_DRIVEN=n +# Enable HW RX byte counting. This especially matters at higher baud rates. +CONFIG_UART_1_NRF_HW_ASYNC=y +CONFIG_UART_1_NRF_HW_ASYNC_TIMER=1 + +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_MODEM_HL78XX_DEV_STARTUP_TIME=1000 +# Disable AT shell as SLM application has no AT mode user pipes +CONFIG_MODEM_AT_SHELL=n +# Increase log buffer size to accommodate large dumps +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BUFFER_SIZE=65535 +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 +# Print logs and printk() output on uart0. +CONFIG_LOG_BACKEND_UART=y +CONFIG_MODEM_LOG_LEVEL_DBG=y diff --git a/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.overlay b/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.overlay new file mode 100644 index 0000000000000..adc03c24afef8 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.overlay @@ -0,0 +1,45 @@ +/ { + aliases { + modem = &modem; + }; +}; + +&uart1 { + compatible = "nordic,nrf-uarte"; + current-speed = <115200>; + hw-flow-control; + status = "okay"; + + pinctrl-0 = <&uart1_default_alt>; + modem: hl_modem { + compatible = "swir,hl7812"; + status = "okay"; + mdm-reset-gpios = <&gpio0 20 (GPIO_ACTIVE_LOW)>; + socket_offload: socket_offload { + compatible = "swir,hl7812-offload"; + status = "okay"; + /* optional properties for future: */ + max-data-length = <512>; + }; + gnss: hl_gnss { + compatible = "swir,hl7812-gnss"; + pps-mode = "GNSS_PPS_MODE_DISABLED"; + fix-rate = <1000>; + status = "okay"; + }; + }; +}; + +&pinctrl { + uart1_default_alt: uart1_default_alt { + group1 { + psels = ; + bias-pull-up; + }; + group2 { + psels = , + , + ; + }; + }; +}; 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 0000000000000..8fad488237a6e --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/overlay-swir_hl78xx-verbose-logging.conf @@ -0,0 +1,8 @@ +# Logging +CONFIG_LOG_BUFFER_SIZE=85536 + +# 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/prj.conf b/samples/drivers/modem/hello_hl78xx/prj.conf new file mode 100644 index 0000000000000..dc69dcd52f02b --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/prj.conf @@ -0,0 +1,76 @@ +# Sierra Wireless HL78XX Sample configuration + +# 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 + +# 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=12000 + +# 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 + +# 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_APN_SOURCE_ICCID=y +# CONFIG_MODEM_HL78XX_APN_PROFILES="hologram=23450, wm=20601, vodafone=8988239, em=8988303" + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +# CONFIG_MODEM_HL78XX_AUTORAT_PRL_PROFILES="2,1,3" +# CONFIG_MODEM_HL78XX_AUTORAT_NB_BAND_CFG="3,8,20,28" +# CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Stay in boot mode until registered to a network +# CONFIG_MODEM_HL78XX_STAY_IN_BOOT_MODE_FOR_ROAMING=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y +# Logging +CONFIG_LOG=y diff --git a/samples/drivers/modem/hello_hl78xx/sample.yaml b/samples/drivers/modem/hello_hl78xx/sample.yaml new file mode 100644 index 0000000000000..625cf998cd748 --- /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 0000000000000..e45fe0eb80990 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/src/main.c @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2025 Netfeasa + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#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) + +#define TEST_SERVER_PORT 6000 +#define TEST_SERVER_ENDPOINT "flake.legato.io" + +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"; +#ifdef CONFIG_MODEM_HL78XX_12 + case HL78XX_RAT_GSM: + return "GSM"; +#ifdef CONFIG_MODEM_HL78XX_12_FW_R6 + case HL78XX_RAT_NBNTN: + return "NTN"; +#endif /* CONFIG_MODEM_HL78XX_12_FW_R6 */ +#endif /* CONFIG_MODEM_HL78XX_12 */ + default: + return "Not ready"; + } +} +/** Convert registration status to string */ +static const char *reg_status_get_in_string(enum cellular_registration_status rat) +{ + switch (rat) { + case CELLULAR_REGISTRATION_NOT_REGISTERED: + return "Not Registered"; + case CELLULAR_REGISTRATION_REGISTERED_HOME: + return "Home Network"; + case CELLULAR_REGISTRATION_SEARCHING: + return "Network Searching"; + case CELLULAR_REGISTRATION_DENIED: + return "Registration Denied"; + case CELLULAR_REGISTRATION_UNKNOWN: + return "Out of coverage or Unknown"; + case CELLULAR_REGISTRATION_REGISTERED_ROAMING: + return "Roaming Network"; + default: + return "Not ready"; + } +} + +/** Convert module status code to string */ +/** Convert hl78xx module status enum to string */ +const char *hl78xx_module_status_to_string(enum hl78xx_module_status status) +{ + switch (status) { + case HL78XX_MODULE_READY: + return "Module is ready to receive commands. No access code required."; + case HL78XX_MODULE_WAITING_FOR_ACCESS_CODE: + return "Module is waiting for an access code."; + case HL78XX_MODULE_SIM_NOT_PRESENT: + return "SIM card is not present."; + case HL78XX_MODULE_SIMLOCK: + return "Module is in SIMlock state."; + case HL78XX_MODULE_UNRECOVERABLE_ERROR: + return "Unrecoverable error."; + case HL78XX_MODULE_UNKNOWN_STATE: + return "Unknown state."; + case HL78XX_MODULE_INACTIVE_SIM: + return "Inactive SIM."; + default: + return "Invalid module status."; + } +} + +/* 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, struct hl78xx_evt_monitor_entry *context) +{ +#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_LTE_RAT_UPDATE: + LOG_INF("%d HL78XX modem rat mode changed: %d", __LINE__, event->content.rat_mode); + break; + case HL78XX_LTE_REGISTRATION_STAT_UPDATE: + LOG_INF("%d HL78XX modem registration status: %d", __LINE__, + event->content.reg_status); + break; + case HL78XX_LTE_MODEM_STARTUP: + LOG_INF("%d HL78XX modem startup status: %s", __LINE__, + hl78xx_module_status_to_string(event->content.value)); + 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 +} + +/** + * @brief resolve_broker_addr - Resolve the broker address and port. + * @param broker Pointer to sockaddr_in structure to store the resolved address. + */ +static int resolve_broker_addr(struct sockaddr_in *broker) +{ + int ret; + struct addrinfo *ai = NULL; + + const struct addrinfo hints = { + .ai_family = AF_INET, + .ai_socktype = SOCK_STREAM, + .ai_protocol = 0, + }; + char port_string[6] = {0}; + + snprintf(port_string, sizeof(port_string), "%d", TEST_SERVER_PORT); + ret = getaddrinfo(TEST_SERVER_ENDPOINT, port_string, &hints, &ai); + if (ret == 0) { + char addr_str[INET_ADDRSTRLEN]; + + memcpy(broker, ai->ai_addr, MIN(ai->ai_addrlen, sizeof(struct sockaddr_storage))); + + inet_ntop(AF_INET, &broker->sin_addr, addr_str, sizeof(addr_str)); + LOG_INF("Resolved: %s:%u", addr_str, htons(broker->sin_port)); + } else { + LOG_ERR("failed to resolve hostname err = %d (errno = %d)", ret, errno); + } + + freeaddrinfo(ai); + + return ret; +} + +MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", hl78xx_on_ok); + +HL78XX_EVT_MONITOR(listener_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 + /* Modem information */ + char manufacturer[MDM_MANUFACTURER_LENGTH] = {0}; + char fw_ver[MDM_REVISION_LENGTH] = {0}; + char apn[MDM_APN_MAX_LENGTH] = {0}; + char operator[MDM_MODEL_LENGTH] = {0}; + char imei[MDM_IMEI_LENGTH] = {0}; + enum hl78xx_cell_rat_mode tech; + enum cellular_registration_status status; + int16_t rsrp; + const char *newapn = ""; + const char *sample_cmd = "AT"; + +#ifndef CONFIG_MODEM_HL78XX_AUTORAT +#if defined(CONFIG_MODEM_HL78XX_RAT_M1) + tech = HL78XX_RAT_CAT_M1; +#elif defined(CONFIG_MODEM_HL78XX_RAT_NB1) + tech = HL78XX_RAT_NB1; +#elif defined(CONFIG_MODEM_HL78XX_RAT_GSM) + tech = HL78XX_RAT_GSM; +#elif defined(CONFIG_MODEM_HL78XX_RAT_NBNTN) + tech = HL78XX_RAT_NBNTN; +#else +#error "No rat has been selected." +#endif +#endif /* MODEM_HL78XX_AUTORAT */ + + cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_MANUFACTURER, manufacturer, + sizeof(manufacturer)); + + cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_FW_VERSION, fw_ver, sizeof(fw_ver)); + + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_APN, (char *)apn, sizeof(apn)); + + cellular_get_modem_info(modem, CELLULAR_MODEM_INFO_IMEI, imei, sizeof(imei)); +#ifdef CONFIG_MODEM_HL78XX_AUTORAT + /* In auto rat mode, get the current rat from the modem status */ + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_CURRENT_RAT, + (enum cellular_access_technology *)&tech, sizeof(tech)); +#endif /* CONFIG_MODEM_HL78XX_AUTORAT */ + /* Get the current registration status */ + cellular_get_registration_status(modem, hl78xx_rat_to_access_tech(tech), &status); + /* Get the current signal strength */ + cellular_get_signal(modem, CELLULAR_SIGNAL_RSRP, &rsrp); + /* Get the current network operator name */ + hl78xx_get_modem_info(modem, HL78XX_MODEM_INFO_NETWORK_OPERATOR, (char *)operator, + sizeof(operator)); + + 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(%d)", reg_status_get_in_string(status), status); + LOG_INF("RSRP : %d", rsrp); + LOG_INF("Operator: %s", (strlen(operator) > 0) ? operator : "\"\""); + LOG_RAW("**********************************************************\n\n"); + + LOG_INF("Setting new APN: %s", newapn); + ret = cellular_set_apn(modem, newapn); + if (ret < 0) { + LOG_ERR("Failed to set new APN, error: %d", ret); + } + + k_sem_reset(&network_connected_sem); + LOG_INF("Waiting for network connection..."); + k_sem_take(&network_connected_sem, K_FOREVER); + + 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 : "\"\""); + + struct sockaddr_in test_endpoint_addr; + + LOG_INF("Test endpoint: %s:%d", TEST_SERVER_ENDPOINT, TEST_SERVER_PORT); + + resolve_broker_addr(&test_endpoint_addr); + + LOG_INF("Sample application finished."); + + return 0; +} diff --git a/samples/drivers/modem/index.rst b/samples/drivers/modem/index.rst new file mode 100644 index 0000000000000..5b7a92c1018fa --- /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 ea39d7c4db42dc9e294e3ca4d9dd9305ca08ec61 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/aws_iot_mqtt: add hl78xx driver config file add support for HL78xx driver Signed-off-by: Zafer SEN --- .../boards/nrf9160dk_nrf9160_ns.conf | 1 - .../aws_iot_mqtt/overlay-swir_hl78xx-tls.conf | 5 ++ .../overlay-swir_hl78xx-verbose-logging.conf | 13 ++++ .../overlay-swir_hl78xx_ev_kit.conf | 64 ++++++++++++++++ samples/net/common/Kconfig | 2 +- .../overlay-swir_hl78xx_ev_kit.conf | 74 +++++++++++++++++++ 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-tls.conf create mode 100644 samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-verbose-logging.conf create mode 100644 samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx_ev_kit.conf create mode 100644 samples/net/lwm2m_client/overlay-swir_hl78xx_ev_kit.conf diff --git a/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.conf b/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.conf index e2cbd919f4b86..d39bd9151a541 100644 --- a/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.conf +++ b/samples/drivers/modem/hello_hl78xx/boards/nrf9160dk_nrf9160_ns.conf @@ -12,7 +12,6 @@ CONFIG_MODEM_HL78XX_DEV_STARTUP_TIME=1000 # Disable AT shell as SLM application has no AT mode user pipes CONFIG_MODEM_AT_SHELL=n # Increase log buffer size to accommodate large dumps -CONFIG_LOG_MODE_DEFERRED=y CONFIG_LOG_BUFFER_SIZE=65535 CONFIG_MODEM_MODULES_LOG_LEVEL_DBG=y CONFIG_MODEM_LOG_LEVEL_DBG=y diff --git a/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-tls.conf b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-tls.conf new file mode 100644 index 0000000000000..23f42c08229f6 --- /dev/null +++ b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-tls.conf @@ -0,0 +1,5 @@ +# socket tls +CONFIG_TLS_CREDENTIALS=y +CONFIG_TLS_MAX_CREDENTIALS_NUMBER=4 +CONFIG_MODEM_HL78XX_ADVANCED_SOCKET_CONFIG=y +CONFIG_MODEM_HL78XX_SOCKETS_SOCKOPT_TLS=y diff --git a/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-verbose-logging.conf b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-verbose-logging.conf new file mode 100644 index 0000000000000..b94b8874c1a42 --- /dev/null +++ b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx-verbose-logging.conf @@ -0,0 +1,13 @@ +# Logging +CONFIG_LOG_BUFFER_SIZE=65535 + +# 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 + +CONFIG_MQTT_LOG_LEVEL_DBG=y +CONFIG_LOG_BACKEND_NET=y +CONFIG_NET_BUF_LOG=y +CONFIG_NET_LOG=y 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 0000000000000..2ebb4456acfcb --- /dev/null +++ b/samples/net/cloud/aws_iot_mqtt/overlay-swir_hl78xx_ev_kit.conf @@ -0,0 +1,64 @@ +# 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 + +# 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 + +# 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" + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +# CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y diff --git a/samples/net/common/Kconfig b/samples/net/common/Kconfig index afaab763ac58f..ceed53efa8692 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 0000000000000..d10805237afe9 --- /dev/null +++ b/samples/net/lwm2m_client/overlay-swir_hl78xx_ev_kit.conf @@ -0,0 +1,74 @@ +# 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 + +# Generic networking options +CONFIG_NET_IPV6=n + +# DNS +CONFIG_DNS_RESOLVER=y +CONFIG_NET_SOCKETS_DNS_TIMEOUT=15000 + +# POSIX API +CONFIG_POSIX_API=y +CONFIG_REQUIRES_FULL_LIBC=y + +# 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 + +# 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" + +# RAT selection +CONFIG_MODEM_HL78XX_AUTORAT=n +# CONFIG_MODEM_HL78XX_RAT_NB1=y + +# Monitor modem events +CONFIG_HL78XX_EVT_MONITOR=y + +# Logging +CONFIG_LOG_BUFFER_SIZE=65535 +# 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 2babbf66e049fda066dae230785f90a1d07b7993 Mon Sep 17 00:00:00 2001 From: Zafer SEN Date: Sun, 8 Jun 2025 22:43:18 +0100 Subject: [PATCH 4/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 ++++++++++++++++++ boards/shields/swir_hl78xx_ev_kit/shield.yml | 6 ++ .../swir_hl78xx_ev_kit.overlay | 43 ++++++++++ dts/bindings/modem/swir,hl7812-gnss.yaml | 12 +++ dts/bindings/modem/swir,hl7812-offload.yaml | 8 ++ dts/bindings/modem/swir,hl7812.yaml | 8 ++ dts/bindings/modem/swir,hl78xx-gnss.yaml | 24 ++++++ dts/bindings/modem/swir,hl78xx-offload.yaml | 32 +++++++ dts/bindings/modem/swir,hl78xx.yaml | 34 ++++++++ 11 files changed, 251 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/shield.yml create mode 100644 boards/shields/swir_hl78xx_ev_kit/swir_hl78xx_ev_kit.overlay create mode 100644 dts/bindings/modem/swir,hl7812-gnss.yaml create mode 100644 dts/bindings/modem/swir,hl7812-offload.yaml create mode 100644 dts/bindings/modem/swir,hl7812.yaml create mode 100644 dts/bindings/modem/swir,hl78xx-gnss.yaml create mode 100644 dts/bindings/modem/swir,hl78xx-offload.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 0000000000000..e5c5d711b3426 --- /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 0000000000000..cc6a9ce761f67 --- /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/shield.yml b/boards/shields/swir_hl78xx_ev_kit/shield.yml new file mode 100644 index 0000000000000..536f2bc030754 --- /dev/null +++ b/boards/shields/swir_hl78xx_ev_kit/shield.yml @@ -0,0 +1,6 @@ +shield: + name: swir_hl78xx_ev_kit + full_name: Sierra Wireless HL/RC Module Evaluation Kit + vendor: Sierra Wireless + supported_features: + - modem 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 0000000000000..f2e7a75ae658f --- /dev/null +++ b/boards/shields/swir_hl78xx_ev_kit/swir_hl78xx_ev_kit.overlay @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Netfeasa Ltd. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + aliases { + modem-uart = &usart2; + modem = &modem; + gnss = &gnss; + }; +}; + +&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)>; + socket_offload: socket_offload { + compatible = "swir,hl7812-offload"; + status = "okay"; + /* optional properties for future: */ + max-data-length = <512>; + }; + gnss: hl_gnss { + compatible = "swir,hl7812-gnss"; + pps-mode = "GNSS_PPS_MODE_DISABLED"; + fix-rate = <1000>; + status = "okay"; + }; + }; + +}; diff --git a/dts/bindings/modem/swir,hl7812-gnss.yaml b/dts/bindings/modem/swir,hl7812-gnss.yaml new file mode 100644 index 0000000000000..5160bd981eb2e --- /dev/null +++ b/dts/bindings/modem/swir,hl7812-gnss.yaml @@ -0,0 +1,12 @@ +description: | + Binding for a modem offload child node that indicates the modem + supports socket offload functionality. This node is intended to be a + child of a modem device node (for example, `modem: hl_modem { ... };`). + + The binding is intentionally small and extensible; it documents a + presence node (compatible = "swir,hl7812-offload") and may be extended + in future with additional properties that the driver may consume. + +compatible: "swir,hl7812-gnss" + +include: swir,hl78xx-gnss.yaml diff --git a/dts/bindings/modem/swir,hl7812-offload.yaml b/dts/bindings/modem/swir,hl7812-offload.yaml new file mode 100644 index 0000000000000..5f2bedf1e552c --- /dev/null +++ b/dts/bindings/modem/swir,hl7812-offload.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025, Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +description: Sierra Wireless HL7812 Modem offload + +compatible: "swir,hl7812-offload" + +include: swir,hl78xx-offload.yaml diff --git a/dts/bindings/modem/swir,hl7812.yaml b/dts/bindings/modem/swir,hl7812.yaml new file mode 100644 index 0000000000000..a75d0c044b400 --- /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-gnss.yaml b/dts/bindings/modem/swir,hl78xx-gnss.yaml new file mode 100644 index 0000000000000..03632e05cd377 --- /dev/null +++ b/dts/bindings/modem/swir,hl78xx-gnss.yaml @@ -0,0 +1,24 @@ +description: | + Binding for a modem child node that indicates the modem supports + GNSS functionality. This node is intended to be a child of a modem + device node (for example, `modem: hl_modem { ... };`). + + The binding is intentionally small and extensible; it documents a + presence node (compatible = "swir,hl78xx-gnss") and may be extended + in future with additional properties that the driver may consume. + +compatible: "swir,hl78xx-gnss" + +include: + - uart-device.yaml + - gnss-nmea-generic.yaml + - gnss-pps.yaml + +properties: + fix-rate: + type: int + default: 1000 + description: | + Initial fix-rate GNSS modem will be operating on. May be adjusted at + run-time through GNSS APIs. Must be greater than 50-ms. + Default is power-on setting. diff --git a/dts/bindings/modem/swir,hl78xx-offload.yaml b/dts/bindings/modem/swir,hl78xx-offload.yaml new file mode 100644 index 0000000000000..8f8c9fa7b6801 --- /dev/null +++ b/dts/bindings/modem/swir,hl78xx-offload.yaml @@ -0,0 +1,32 @@ +description: | + Binding for a modem child node that indicates the modem supports + socket offloading. This node is intended to be a child of a modem + device node (for example, `modem: hl_modem { ... };`). + + The binding is intentionally small and extensible; it documents a + presence node (compatible = "net,offload-modem-sockets") and a couple + of optional integer properties that the driver may consume. + +compatible: "swir,hl78xx-offload" + +properties: + max-data-length: + type: int + description: | + "Maximum length of a single data payload (bytes) that + the modem can send/receive in one offload operation." + enum: + - 512 + - 1024 + - 2048 + - 4096 + - 8192 + + offload-priority: + type: int + description: | + "Priority of this offload modem compared to other offload + modems in the system. Lower values indicate higher priority. + The system will prefer to use the offload modem with the + highest priority (lowest value) when multiple offload modems + are available." diff --git a/dts/bindings/modem/swir,hl78xx.yaml b/dts/bindings/modem/swir,hl78xx.yaml new file mode 100644 index 0000000000000..b4b1ceef8652b --- /dev/null +++ b/dts/bindings/modem/swir,hl78xx.yaml @@ -0,0 +1,34 @@ +# Copyright (c) 2025, Netfeasa Ltd. +# SPDX-License-Identifier: Apache-2.0 + +description: Sierra Wireless HL78XX Modem + +compatible: "swir,hl78xx" + +include: + - zephyr,cellular-modem-device.yaml + +properties: + mdm-pwr-on-gpios: + type: phandle-array + + mdm-fast-shutd-gpios: + type: phandle-array + + mdm-vgpio-gpios: + type: phandle-array + + mdm-uart-dsr-gpios: + type: phandle-array + + mdm-uart-cts-gpios: + type: phandle-array + + mdm-gpio6-gpios: + type: phandle-array + + mdm-gpio8-gpios: + type: phandle-array + + mdm-sim-switch-gpios: + type: phandle-array From d2f47fb92c7f83f90d45687feaf548b30cfcd56c Mon Sep 17 00:00:00 2001 From: Ryan Erickson Date: Wed, 8 Oct 2025 10:48:39 -0500 Subject: [PATCH 5/7] samples: drivers: modem: hello_hl78xx: add support for pinnacle 100 Add support for Pinnacle 100 modem (HL7800) platforms. Signed-off-by: Ryan Erickson --- .../modem/hello_hl78xx/boards/mg100.conf | 1 + .../modem/hello_hl78xx/boards/mg100.overlay | 1 + .../boards/pinnacle_100_common.dtsi | 36 +++++++++++++++++++ .../hello_hl78xx/boards/pinnacle_100_dvk.conf | 1 + .../boards/pinnacle_100_dvk.overlay | 1 + 5 files changed, 40 insertions(+) create mode 100644 samples/drivers/modem/hello_hl78xx/boards/mg100.conf create mode 100644 samples/drivers/modem/hello_hl78xx/boards/mg100.overlay create mode 100644 samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_common.dtsi create mode 100644 samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.conf create mode 100644 samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.overlay diff --git a/samples/drivers/modem/hello_hl78xx/boards/mg100.conf b/samples/drivers/modem/hello_hl78xx/boards/mg100.conf new file mode 100644 index 0000000000000..8faf74132ae1b --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/mg100.conf @@ -0,0 +1 @@ +CONFIG_MODEM_HL7800=n diff --git a/samples/drivers/modem/hello_hl78xx/boards/mg100.overlay b/samples/drivers/modem/hello_hl78xx/boards/mg100.overlay new file mode 100644 index 0000000000000..63bfd0e5d5ffa --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/mg100.overlay @@ -0,0 +1 @@ +#include "pinnacle_100_common.dtsi" diff --git a/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_common.dtsi b/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_common.dtsi new file mode 100644 index 0000000000000..f0ac16dcefd29 --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_common.dtsi @@ -0,0 +1,36 @@ +/delete-node/ &hl7800; + +/ { + aliases { + modem-uart = &uart1; + modem = &hl7800; + gnss = &gnss; + }; +}; + +&uart1 { + hl7800: hl7800 { + compatible = "swir,hl7800"; + status = "okay"; + mdm-reset-gpios = <&gpio1 15 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-wake-gpios = <&gpio1 13 (GPIO_OPEN_SOURCE | GPIO_ACTIVE_HIGH)>; + mdm-pwr-on-gpios = <&gpio1 2 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-fast-shutd-gpios = <&gpio1 14 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-vgpio-gpios = <&gpio1 11 0>; + mdm-uart-dsr-gpios = <&gpio0 25 0>; + mdm-uart-cts-gpios = <&gpio0 15 0>; + mdm-gpio6-gpios = <&gpio1 12 0>; + socket_offload: socket_offload { + compatible = "swir,hl7812-offload"; + status = "okay"; + /* optional properties for future: */ + max-data-length = <512>; + }; + gnss: hl_gnss { + compatible = "swir,hl7812-gnss"; + pps-mode = "GNSS_PPS_MODE_DISABLED"; + fix-rate = <1000>; + status = "okay"; + }; + }; +}; diff --git a/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.conf b/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.conf new file mode 100644 index 0000000000000..8faf74132ae1b --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.conf @@ -0,0 +1 @@ +CONFIG_MODEM_HL7800=n diff --git a/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.overlay b/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.overlay new file mode 100644 index 0000000000000..63bfd0e5d5ffa --- /dev/null +++ b/samples/drivers/modem/hello_hl78xx/boards/pinnacle_100_dvk.overlay @@ -0,0 +1 @@ +#include "pinnacle_100_common.dtsi" From b6e619e0c67891f108a3fb6c5aadab2373df9814 Mon Sep 17 00:00:00 2001 From: Ryan Erickson Date: Wed, 8 Oct 2025 11:04:22 -0500 Subject: [PATCH 6/7] samples: net: aws_iot_mqtt: support Pinnacle 100 with HL78xx driver Add support for using the HL78XX modem driver with Pinnacle 100 modem boards. Signed-off-by: Ryan Erickson --- .../overlay-pinnacle_100-hl78xx.conf | 4 +++ .../aws_iot_mqtt/pinnacle_100-hl78xx.overlay | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 samples/net/cloud/aws_iot_mqtt/overlay-pinnacle_100-hl78xx.conf create mode 100644 samples/net/cloud/aws_iot_mqtt/pinnacle_100-hl78xx.overlay diff --git a/samples/net/cloud/aws_iot_mqtt/overlay-pinnacle_100-hl78xx.conf b/samples/net/cloud/aws_iot_mqtt/overlay-pinnacle_100-hl78xx.conf new file mode 100644 index 0000000000000..4c4dcdabd740c --- /dev/null +++ b/samples/net/cloud/aws_iot_mqtt/overlay-pinnacle_100-hl78xx.conf @@ -0,0 +1,4 @@ +CONFIG_MODEM_HL7800=n +CONFIG_UART_ASYNC_API=y +CONFIG_MODEM_HL78XX=y +CONFIG_MODEM_HL78XX_AUTORAT=n diff --git a/samples/net/cloud/aws_iot_mqtt/pinnacle_100-hl78xx.overlay b/samples/net/cloud/aws_iot_mqtt/pinnacle_100-hl78xx.overlay new file mode 100644 index 0000000000000..c5cdf23107e83 --- /dev/null +++ b/samples/net/cloud/aws_iot_mqtt/pinnacle_100-hl78xx.overlay @@ -0,0 +1,36 @@ +/delete-node/ &hl7800; + +/ { + aliases { + modem-uart = &uart1; + modem = &hl7800; + gnss = &gnss; + }; +}; + +&uart1 { + hl7800: hl7800 { + compatible = "swir,hl7800"; + status = "okay"; + mdm-reset-gpios = <&gpio1 15 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-wake-gpios = <&gpio1 13 (GPIO_OPEN_SOURCE | GPIO_ACTIVE_HIGH)>; + mdm-pwr-on-gpios = <&gpio1 2 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-fast-shutd-gpios = <&gpio1 14 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-vgpio-gpios = <&gpio1 11 0>; + mdm-uart-dsr-gpios = <&gpio0 25 0>; + mdm-uart-cts-gpios = <&gpio0 15 0>; + mdm-gpio6-gpios = <&gpio1 12 0>; + socket_offload: socket_offload { + compatible = "swir,hl7812-offload"; + status = "okay"; + /* optional properties for future: */ + max-data-length = <512>; + }; + gnss: hl_gnss { + compatible = "swir,hl7812-gnss"; + pps-mode = "GNSS_PPS_MODE_DISABLED"; + fix-rate = <1000>; + status = "disabled"; + }; + }; +}; From 98abcc1da4ea57e301ab45b4342511cb0d146e51 Mon Sep 17 00:00:00 2001 From: Ryan Erickson Date: Wed, 8 Oct 2025 15:28:49 -0500 Subject: [PATCH 7/7] samples: net: lwm2m_client: support Pinnacle 100 with HL78xx driver Add support for using the HL78XX modem driver with Pinnacle 100 modem boards. Signed-off-by: Ryan Erickson --- samples/net/lwm2m_client/Kconfig | 2 +- .../overlay-pinnacle_100-hl78xx.conf | 27 ++++++++++++++ .../lwm2m_client/pinnacle_100-hl78xx.overlay | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 samples/net/lwm2m_client/overlay-pinnacle_100-hl78xx.conf create mode 100644 samples/net/lwm2m_client/pinnacle_100-hl78xx.overlay diff --git a/samples/net/lwm2m_client/Kconfig b/samples/net/lwm2m_client/Kconfig index 9625cb1b949fe..7952bf39b1887 100644 --- a/samples/net/lwm2m_client/Kconfig +++ b/samples/net/lwm2m_client/Kconfig @@ -47,7 +47,7 @@ config NET_SAMPLE_LWM2M_SERVER config NET_SAMPLE_LWM2M_WAIT_DNS 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-pinnacle_100-hl78xx.conf b/samples/net/lwm2m_client/overlay-pinnacle_100-hl78xx.conf new file mode 100644 index 0000000000000..0743f4d999207 --- /dev/null +++ b/samples/net/lwm2m_client/overlay-pinnacle_100-hl78xx.conf @@ -0,0 +1,27 @@ +CONFIG_MODEM_HL7800=n +CONFIG_UART_ASYNC_API=y +CONFIG_MODEM_HL78XX=y +CONFIG_MODEM_HL78XX_AUTORAT=n + +# The HL78xx driver gets its IP settings from the cell network +CONFIG_NET_CONFIG_SETTINGS=n +CONFIG_NET_CONNECTION_MANAGER=y +CONFIG_NET_SAMPLE_LWM2M_WAIT_DNS=y +CONFIG_DNS_RESOLVER=y + +# Generic networking options +CONFIG_NET_IPV6=n + +# NB-IoT has large latency, so increase timeouts. It is ok to use this for Cat-M1 as well. +CONFIG_NET_SOCKETS_DNS_TIMEOUT=12000 +CONFIG_NET_SOCKETS_CONNECT_TIMEOUT=13000 +CONFIG_NET_SOCKETS_DTLS_TIMEOUT=15000 +CONFIG_COAP_INIT_ACK_TIMEOUT_MS=15000 + +# Logging +# CONFIG_LOG_BUFFER_SIZE=65535 +# For extra debug +# 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/net/lwm2m_client/pinnacle_100-hl78xx.overlay b/samples/net/lwm2m_client/pinnacle_100-hl78xx.overlay new file mode 100644 index 0000000000000..c5cdf23107e83 --- /dev/null +++ b/samples/net/lwm2m_client/pinnacle_100-hl78xx.overlay @@ -0,0 +1,36 @@ +/delete-node/ &hl7800; + +/ { + aliases { + modem-uart = &uart1; + modem = &hl7800; + gnss = &gnss; + }; +}; + +&uart1 { + hl7800: hl7800 { + compatible = "swir,hl7800"; + status = "okay"; + mdm-reset-gpios = <&gpio1 15 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-wake-gpios = <&gpio1 13 (GPIO_OPEN_SOURCE | GPIO_ACTIVE_HIGH)>; + mdm-pwr-on-gpios = <&gpio1 2 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-fast-shutd-gpios = <&gpio1 14 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + mdm-vgpio-gpios = <&gpio1 11 0>; + mdm-uart-dsr-gpios = <&gpio0 25 0>; + mdm-uart-cts-gpios = <&gpio0 15 0>; + mdm-gpio6-gpios = <&gpio1 12 0>; + socket_offload: socket_offload { + compatible = "swir,hl7812-offload"; + status = "okay"; + /* optional properties for future: */ + max-data-length = <512>; + }; + gnss: hl_gnss { + compatible = "swir,hl7812-gnss"; + pps-mode = "GNSS_PPS_MODE_DISABLED"; + fix-rate = <1000>; + status = "disabled"; + }; + }; +};