diff --git a/samples/cellular/nrf_cloud_multi_service/CMakeLists.txt b/samples/cellular/nrf_cloud_multi_service/CMakeLists.txt index 75563a53f7b3..b050a9952f8f 100644 --- a/samples/cellular/nrf_cloud_multi_service/CMakeLists.txt +++ b/samples/cellular/nrf_cloud_multi_service/CMakeLists.txt @@ -1,11 +1,13 @@ # -# Copyright (c) 2022 Nordic Semiconductor ASA +# Copyright (c) 2024 Nordic Semiconductor ASA # # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause # cmake_minimum_required(VERSION 3.20.0) +set(BOARD_ROOT ${CMAKE_CURRENT_LIST_DIR}) + find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(nrf_cloud_multi_service) zephyr_compile_definitions(PROJECT_NAME=${PROJECT_NAME}) @@ -45,6 +47,19 @@ if(CONFIG_NRF_MODEM_LIB) target_sources(app PRIVATE src/at_commands.c) endif() -# NORDIC SDK APP END - zephyr_include_directories(src) + +if (CONFIG_NRF_CLOUD_GATEWAY) + target_sources(app PRIVATE src/ble/ble.c) + target_sources(app PRIVATE src/ble/ble_codec.c) + target_sources(app PRIVATE src/ble/ble_conn_mgr.c) + target_sources(app PRIVATE src/ble/gateway.c) + target_sources_ifdef(CONFIG_SHELL app PRIVATE src/ble/cli.c) + target_sources_ifdef(CONFIG_GATEWAY_BLE_FOTA app PRIVATE src/ble/dfu/peripheral_dfu.c) + target_sources_ifdef(CONFIG_FLASH_TEST, app PRIVATE src/ble/flash/flash_test.c) + zephyr_include_directories(src src/ble src/ble/flash src/ble/dfu) + #add_subdirectory(src/ble/dfu) + add_subdirectory(src/ble/flash) +endif() + +# NORDIC SDK APP END diff --git a/samples/cellular/nrf_cloud_multi_service/Kconfig b/samples/cellular/nrf_cloud_multi_service/Kconfig index acb98381f0c1..22e8eb85b3ad 100644 --- a/samples/cellular/nrf_cloud_multi_service/Kconfig +++ b/samples/cellular/nrf_cloud_multi_service/Kconfig @@ -6,6 +6,8 @@ menu "Multi Service Sample Settings" +rsource "Kconfig.ble_gateway" + config APP_VERSION string "Multi Service Sample version" default "1.0.0" diff --git a/samples/cellular/nrf_cloud_multi_service/Kconfig.ble_gateway b/samples/cellular/nrf_cloud_multi_service/Kconfig.ble_gateway new file mode 100644 index 000000000000..201d6d79ae8f --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/Kconfig.ble_gateway @@ -0,0 +1,139 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "nRF Cloud BLE Gateway Sample Settings" + +if NRF_CLOUD_GATEWAY + +config USE_BT_HCI_SETUP + bool "Use BT HCI setup" + default y + select BT_HCI_SETUP + help + Get the setup to run. + +config APPLICATION_WORKQUEUE_PRIORITY + int "Application workqueue priority" + default SYSTEM_WORKQUEUE_PRIORITY + +config FLASH_TEST + bool "Enable Flash Test module" + default n + select NRFX_SPIM if FLASH_TEST + select NRFX_SPIM3 if FLASH_TEST + select SPI if FLASH_TEST + select SPI_NOR if FLASH_TEST + help + Enable to test Erase, Write, and Read of external flash. + +config CLOUD_BUTTON + bool "Enable button sensor" + default y + +config CLOUD_BUTTON_INPUT + int "Set button sensor button number" + range 1 4 if BOARD_NRF9160_PCA10090NS + range 1 1 if BOARD_NRF9160_PCA20035NS + default 1 + +config ENTER_52840_MCUBOOT_VIA_BUTTON + bool "Button enables 52840 MCUboot" + depends on BOARD_APRICITY_GATEWAY_NRF9160 || BOARD_APRICITY_GATEWAY_NRF9160_NS + help + Holding a button during and after startup will place the + nrf52840 into MCUboot mode so the firmware can be updated. + +config SHELL_PROMPT_SECURE + string "Displayed prompt name" + default "uart:~# " + help + Displayed prompt name for UART backend once user has logged in. + +config SHELL_DEFAULT_PASSWORD + string "Factory-set initial password" + default "nordic" + help + User needs to type this to enter the shell and execute + commands. + +config GATEWAY_BLE_FOTA + bool "Enable BLE FOTA support" + default y + help + Enable ability to receive FOTA jobs for specific BLE devices, + then inject data from one or more file downloads to a BLE + device that supports a compatible DFU protocol. + +config GATEWAY_DBG_CMDS + bool "Enable debugging commands" + default y + select CPU_LOAD + select CPU_LOAD_CMDS + select KERNEL_SHELL + +config GATEWAY_SHELL + bool "Enable shell for administering nRF Cloud gateway" + default y + select SHELL + select SHELL_CMDS + select SHELL_HISTORY + select SHELL_METAKEYS + select SHELL_VT100_COLORS + select SHELL_BACKEND_SERIAL + select SHELL_CMDS_SELECT + +config STARTING_LOG_OVERRIDE + bool "Different logging level at boot" + default n + help + All configured logging will occur if disabled. Otherwise, + logging changed to STARTING_LOG_LEVEL until user logs in and + manually enables with 'log enable inf' (or whatever level is + desired). + +choice + prompt "Max compiled-in log level to boot with" + default STARTING_LOG_LEVEL_INF + depends on LOG + +config STARTING_LOG_LEVEL_OFF + bool "Off" + +config STARTING_LOG_LEVEL_ERR + bool "Error" + +config STARTING_LOG_LEVEL_WRN + bool "Warning" + +config STARTING_LOG_LEVEL_INF + bool "Info" + +config STARTING_LOG_LEVEL_DBG + bool "Debug" + +endchoice + +config STARTING_LOG_LEVEL + int + depends on LOG + default 0 if STARTING_LOG_LEVEL_OFF + default 1 if STARTING_LOG_LEVEL_ERR + default 2 if STARTING_LOG_LEVEL_WRN + default 3 if STARTING_LOG_LEVEL_INF + default 4 if STARTING_LOG_LEVEL_DBG + +if NRF_CLOUD_MQTT +module-str = MQTT BLE Gateway +endif +if NRF_CLOUD_COAP +module-str = CoAP BLE Gateway +endif +module = NRFCLOUD_BLE_GATEWAY +source "subsys/logging/Kconfig.template.log_config" + +endif + +endmenu diff --git a/samples/cellular/nrf_cloud_multi_service/README.rst b/samples/cellular/nrf_cloud_multi_service/README.rst index e120c4e8755b..a240d731b13d 100644 --- a/samples/cellular/nrf_cloud_multi_service/README.rst +++ b/samples/cellular/nrf_cloud_multi_service/README.rst @@ -42,7 +42,7 @@ This sample implements or demonstrates the following features: * Support for the `nRF Cloud Provisioning Service`_ using the :ref:`lib_nrf_provisioning` library. For compatibility with auto-onboarding, the device ID uses the 128 bit UUID format rather than the older nrf- format. * Support for remote execution of modem AT commands using application-specific device messages. -* Periodic cellular, Wi-FiĀ®, and GNSS location tracking using the :ref:`lib_location` library. +* Periodic cellular, Wi-Fi, and GNSS location tracking using the :ref:`lib_location` library. * Periodic temperature sensor sampling on your `Nordic Thingy:91`_, or fake temperature measurements on your `nRF9151 DK `_ , `nRF9161 DK `_, or `nRF9160 DK `_. * Transmission of sensor and GNSS location samples to the nRF Cloud portal as `nRF Cloud device messages `_. * Construction of valid `nRF Cloud device messages `_. diff --git a/samples/cellular/nrf_cloud_multi_service/README_GATEWAY.rst b/samples/cellular/nrf_cloud_multi_service/README_GATEWAY.rst new file mode 100644 index 000000000000..5764b7f54ee4 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/README_GATEWAY.rst @@ -0,0 +1,546 @@ +.. _nrfcloud_ble_gateway: + +nRF9160: nRF Cloud BLE Gateway +############################## + +The nRF Cloud BLE Gateway uses the `lib_nrf_cloud`_ to connect an nRF9160-based board to `nRF Cloud`_ via LTE, connnect to multiple Bluetooth LE peripherals, and transmit their data to the cloud. +Therefore, this application acts as a gateway between Bluetooth LE and the LTE connection to nRF Cloud. + +Overview +******** + +The application uses the LTE link control driver to establish a network connection. +It is then able to connect to multiple Bluetooth LE peripherals, and can then transmit the peripheral data to Nordic Semiconductor's cloud solution, `nRF Cloud`_. +The data is visualized in nRF Cloud's web interface. + +Programs such as PuTTY_, `Tera Term`_, or the `LTE Link Monitor`_ application, implemented as part of `nRF Connect for Desktop`_, can be used to interact with the included shell. +You can also send AT commands from the **Terminal** card on nRF Cloud when the device is connected. + +By default, the gateway supports firmware updates through `lib_nrf_cloud_fota`_. + +.. _nrfcloud_ble_gateway_requirements: + +Requirements +************ + +* One of the following boards: + + * Apricity Gateway + * `nRF9160 DK `_ + +* For the Apricity Gateway nRF9160, `nrfcloud_gateway_controller`_ must be programmed in the nRF52 board controller. +* For the nRF9160 DK, `hci_lpuart`_ must instead be programmed in the nRF52 board controller. +* The sample is configured to compile and run as a non-secure application on nRF91's Cortex-M33. + Therefore, it automatically includes the `secure_partition_manager`_ that prepares the required peripherals to be available for the application. + + +.. _nrfcloud_ble_gateway_user_interface: + +Key Features +************ +Hardware +-------- +- Runs on either the Apricity Gateway hardware or the nRF9160DK +- 700-960 MHz + 1710-2200 MHz LTE band support. + The following bands, based on geographic regions, are used: + - USA 2, 4, 12, and 13 + - EU 3, 8, 20, and 28 +- Certifications: CE, FCC +- LTE-M/NB-IoT and Bluetooth LE antennas +- Nano/4FF Subscriber Identity Module (SIM) card slot +- 64 megabit SPI serial Flash memory +- PC connection through USB +- Normal operating temperature range: 5C ~ 35C + +Apricity Gateway hardware distinguishing features: + +- Single button with multiple uses power, reset, and update enable +- Dual RGB LEDs for status indication of LTE and BLE connections +- Larger LTE and BLE antennas +- Rechargeable 3.7V Li-Po battery with 2000 mAh capacity +- Charging through Universal Serial Bus (USB) or external power supply with barrel jack connector +- Rigid, wall mountable enclosure + +Firmware +-------- +- Supports up to 8 [#]_ BLE Devices +- Supports FOTA (firmware over the air) updates of the nRF9160 modem, bootloader, and application +- Supports FOTA updates of select BLE devices +- Supports USB updates of nRF52840 processor +- Offers shell interface with secure login for USB serial management of gateway + +.. [#] design improvements are planned to enable more. + +User interface +************** + +One can locally manage the gateway with a USB connection and a terminal program. +See :ref:`Shell` for details. + +The Apricity Gateway button has the following functions: + + * Power off device when held for more than one second and released before reset. + * Short press again will power on the device. + * Reset device when held for > 7.5 seconds. + * Enter USB MCUboot update mode for nrf52840 when held for > 11.5 seconds. + +The application state is indicated by the LEDs. + +.. list-table:: + :header-rows: 1 + :align: center + + * - LTE LED 1 color + - State + * - Off + - Not connected to LTE carrier + * - Slow White Pulse + - Connecting to LTE carrier + * - Slow Yellow Pulse + - Associating with nRF Cloud + * - Slow Cyan Pulse + - Connecting to nRF Cloud + * - Solid Blue + - Connected, ready for BLE connections + * - Red Pulse + - Error + +.. list-table:: + :header-rows: 1 + :align: center + + * - BLE LED 2 color + - State + * - Slow Purple Pulse + - Button being held; continue to hold to enter nRF52840 USB MCUboot update mode + * - Rapid Purple Pulse + - in nRF52840 USB MCUboot update mode + * - Slow Yellow Pulse + - Waiting for Bluetooth LE device to connect + * - Solid White + - Bluetooth LE connection established + +Building and running +******************** + +In order to Flash the first firmware image to the Apricity Gateway, you will need one of the following connections: + + - An nRF9160 DK with VDDIO set to 3V, a 10 pin ribbon connected to Debug out, and an adapter from that to a 6 pin Tag Connect connector. + - A Segger J-Link with an adapter to a 6 pin Tag Connect. + - For either method, connect the Tag Connect to ``NRF91:J1`` on the PCB. + +For programming to run on the nRF9160 DK, set ``PROG/DEBUG`` to ``nRF91``. + +Program nRF9160 Application Processor +------------------------------------- + +1. Checkout this repository. +#. Execute the following to pull down all other required repositories:: + + west update + +#. Execute the following to build for the Apricity Gateway hardware:: + + west build -d build_ag -b apricity_gateway_nrf9160ns + +#. Or execute this, to build for the nRF9160 DK:: + + west build -d build_dk -b nrf9160dk_nrf9160_ns + +#. Flash to either board, replacing with the value above after the ``-d`` option:: + + west flash -d --erase --force + +Program nRF52840 Board Controller +--------------------------------- + +`nrfcloud_gateway_controller`_ + +For the Apricity Gateway hardware, follow the same instructions as above in the folder for its repository, except use ``apricity_gateway_nrf52840`` instead of ``apricity_gateway_nrf9160ns``, and connect the Tag Connect to ``NRF52:J1``. + +For the nRF9160 DK, `hci_lpuart`_ must instead be programmed in the nRF52 board controller. +This should be done from the root of the lte-gateway repo so that the required device tree overlays in the `boards <./boards>`_ folder are utilized. + +Program The nRF9160 Modem Processor +----------------------------------- + +`Modem Firmware v1.3.0`_ + +For either the Apricity Gateway or the nRF9160 DK, you must also flash the modem firmware. +Version ``mfw_1.3.0`` or higher is required. +Program this using `nRF Connect Programmer`_ application. + + +Generating Certificates +*********************** + +An nRF Cloud BLE Gateway must have proper security certificates in order to connect to nRF Cloud. + +Create Self-Signed CA Certs +--------------------------- +This step is done using the `Create CA Cert`_ Python 3 script. + +Check out this repository, install the specified prerequisite Python 3 packages, and then follow the instructions to create a CA cert. +This only needs to be done once per customer. + +Install Device Certificates +--------------------------- +This step is done using the `Device Credentials Installer`_ Python 3 script. + +Here is an example of running this script (replace the CA0x522... values with the file names for your CA certs):: + + $ python3 device_credentials_installer.py -g --ca CA0x522400c80ef6d95ea65ef4860d12adc1b031aa9_ca.pem --ca_key CA0x522400c80ef6d95ea65ef4860d12adc1b031aa9_prv.pem --csv provision.csv -d -F "APP|MODEM|BOOT" + +Using the generated ``provision.csv`` file, go on to the next step. + +NOTES: + +- The ``-A`` (all ports) option will be necessary if using the nRF9160DK, in order to find the board. + If you have more than one board powered on and connected to your PC via USB, you will need to select which board to use. + Otherwise, it will use the first one detected. +- The ``-g`` (gateway) option forces the program to assume this device has a shell which uses the expected Gateway commands in order to send and receive modem ``AT`` commands. + Usually the program will detect this automatically. +- The ``-a`` (append) option allows you to build up a CSV file for multiple devices, and then add them all at once to your account with a single curl command. +- The ``-d`` (delete) option will remove any previous certificates from the ``SECTAG`` being used. + A ``SECTAG`` is a programming slot for certificates in the modem. + The ``SECTAG`` here must match the ``CONFIG_NRF_CLOUD_SEC_TAG`` Kconfig option in the prj.conf file. + +The provision.csv file lists the device ID (UUID) in the first column. +It is also displayed by the Device Credentials Installer script. + +Provisioning and Associating with nRF Cloud +******************************************* + +Once you are signed in, perform the following steps to add the gateway to your nRF Cloud account. + +There are two ways to provision and associate using the provision.csv file you generated:: + +1. Via the nRF Cloud website: `nRF Cloud Provision Devices`_ +#. Programmatically using `nRF Cloud ProvisionDevices REST API`_ + +On the `nRF Cloud Provision Devices`_ page, you can drag and drop the CSV file, or click the button to browse for and select it. +Click the Upload and Provision button to begin the process. +The status will be displayed in the table below. + +Instead of using the website, you can instead use curl or Postman to submit the csv file to the `nRF Cloud ProvisionDevices REST API`_ directly. +You will need to find your nRF Cloud account API Key on your account settings page, and use it in place of $API_KEY below. + +e.g.:: + + $ curl --location --request POST 'https://api.nrfcloud.com/v1/devices' --header 'Authorization: Bearer $API_KEY' --header 'Content-Type: text/csv' --data-binary '@provision.csv' + +*returns*:: + + {"bulkOpsRequestId":"01FE6M2552H7YZQ4XAGWJPR2TW"} + $ + +You can then determine if it succeeded by passing the bulkOpsRequestId returned to the `nRF Cloud FetchBulkOpsRequest REST API`_. + +e.g.:: + + $ curl --location --request GET 'https://api.nrfcloud.com/v1/bulk-ops-requests/01FE6M2552H7YZQ4XAGWJPR2TW' --header 'Authorization: Bearer $API_KEY' + +*returns*:: + + {"bulkOpsRequestId":"01FE6M2552H7YZQ4XAGWJPR2TW","status":"SUCCEEDED","endpoint":"PROVISION_DEVICES","requestedAt":"2021-10-08T19:42:45.992Z","completedAt":"2021-10-08T19:42:49.069Z","uploadedDataUrl":"https://bulk-ops-requests.nrfcloud.com/f08f15c3-b523-7841-ec5a-b277610ade88/provision_devices/01FE6M2552H7YZQ4XAGWJPR2TW.csv"} + $ + +Testing +******* + +After programming the application and all prerequisites to your board, test the Apricity Gateway application by performing the following steps: + +1. Connect the board to the computer using a USB cable. The board is assigned a COM port (Windows) or ttyACM or ttyS device (Linux). + +#. Connect to the board with a terminal emulator, for example, PuTTY, Tera Term, or LTE Link Monitor. + Turn off local echo. Output CR and LF when either is received. + The shell uses VT100-compatible escape sequences for coloration. +#. Reset the board if it was already on. +#. Observe in the terminal window that the board starts up in the Secure Partition Manager and that the application starts. + This is indicated by output similar to the following lines:: + + *** Booting Zephyr OS build v2.6.99-ncs1-rc2-5-ga64e96d17cc7 *** + ... + SPM: prepare to jump to Non-Secure image. + + login: + +#. For PuTTY_ or `LTE Link Monitor`_, you will need to reconnect terminal. + (Bluetooth LE HCI control resets the terminal output and needs to be reconnected). + `Tera Term`_ automatically reconnects. +#. Log in with the default password:: + + nordic + +#. If you wish to see logging messages other than ERROR, such as INFO, execute:: + + log enable inf + +#. Open a web browser and navigate to https://nrfcloud.com/. + Click on Device Management then Gateways. + Click on your device's Device ID (UUID), which takes you to the detailed view of your gateway. +#. The first time you start the application, the device will be added to your account automatically. + + a. Observe that the LED(s) indicate that the device is connected. + #. If the LED(s) indicate an error, check the details of the error in the terminal window. + +#. Add BLE devices by clicking on the + sign. + Read, write, and enable notifications on connected peripheral and observe data being received on the nRF Cloud. +#. Optionally send AT commands from the terminal, and observe that the response is received. + + +.. _Shell: + +Using the Management Interface (Shell) +************************************** +The shell is available via a USB serial port, through one of the two provided serial connections. +Using a 3rd party terminal program such as PuTTY_ or `Tera Term`_, you can log in and administer the gateway directly. + +Once logged in at the login: prompt, you can get help using the tab key or by typing help. :: + + login: ****** + nRF Cloud Gateway + Hit tab for help. + gateway:# + at ble clear cpu_load date exit fota + help history info kernel log login logout + passwd reboot resize session shell shutdown + gateway:# + +Many commands have sub commands. +The shell offers command completion. +Type the start of a command and hit tab, and it will offer available choices. :: + + at - | exit> Execute an AT command. Use + first to remain in AT command mode until 'exit'. + +This command, plus the parameter 'enable' places the shell into AT command mode. +In this mode, the only available commands are nrf9160 +modem AT commands or 'exit' to return to normal shell mode. +See the `nRF91 AT Command Reference`_ for more information about AT commands. :: + + ble - Bluetooth commands + Subcommands: + scan: Scan for BLE devices. + save: Save desired connections to shadow on nRF Cloud. + conn: Connect to BLE device(s). + disc: Disconnect BLE device(s). + en: Enable notifications on + BLE device(s). + dis: Disable notifications on + BLE device(s). + fota: [ver] [crc] [sec_tag] + [frag_size] [apn] start BLE firmware over-the-air update. + test: Set BLE FOTA download test mode. + + clear - Clear the terminal. + cpu_load - debug command to see how busy the nrf9160 is. + date - > - display or change the current + date and time. + exit - leave AT command mode. + fota - [sec_tag] [frag_size] [apn] start firmware + over-the-air update. + history - display previous commands entered in the shell. Up arrow + will move backward one by one, making it easy to repeat + previous commands. + + info - Informational commands + Subcommands: + cloud: Cloud information. + ctlr: BLE controller information. + conn: Connected Bluetooth devices information. + gateway: Gateway information. + irq: Dump IRQ table. + list: List known BLE MAC addresses. + modem: Modem information. + param: List parameters. + scan: Bluetooth scan results. + + kernel - Kernel commands + Subcommands: + cycles: Kernel cycles. + reboot: Reboot. + stacks: List threads stack usage. + threads: List kernel threads. + uptime: Kernel uptime. + version: Kernel version. + + log - Commands for controlling logger + Subcommands: + backend: Logger backends commands. + disable: 'log disable .. ' disables + logs in specified modules (all if no modules + specified). + enable: 'log enable ... ' + enables logs up to given level in specified modules + (all if no modules specified). + go: Resume logging + halt: Halt logging + list_backends: Lists logger backends. + status: Logger status + + login - the default first command after reboot and after logout. + logout - lock shell until user logs in again with correct password. + passwd - change the password. + reboot - disconnect from nRF Cloud and the LTE network, then restart. + resize - update the shell's information about the current dimensions + of the terminal window. + session - display or change the persistent sessions setting. + + shell - Useful, not Unix-like shell commands. + Subcommands: + backspace_mode: Toggle backspace key mode. + Some terminals are not sending separate escape code + for backspace and delete button. This command forces + shell to interpret delete key as backspace. + colors: Toggle colored syntax. + echo: Toggle shell echo. + stats: Shell statistics. + + shutdown - Disconnect from nRF Cloud and the LTE network, then + power down the gateway. Press the button to restart. + + +Updating Firmware with FOTA +*************************** + +The nRF9160 modem, application firmware, and bootloader, can all be updated over the air by nRF Cloud. +This can be done when the gateway is connected to the cloud via https://nrfcloud.com/#/updates-dashboard. + +Modem +----- +Incremental modem updates are supported. +Full modem updates using external flash memory on board the Apricity gateway hardware is possible but not enabled yet. + +nRF9160 +------- +Application and MCUboot firmware updates are supported. + +nRF52840 +-------- +The nRF52840 cannot be updated over the air yet; it must be updated over USB on the Apricity Gateway. +See the Button Behavior and LED Indicator Behavior sections for the process to enter MCUboot mode. +Once in that mode, nRF Connect Desktop Programmer can update the firmware using the MCUBoot mode, by clicking the Enable MCUBoot checkbox. + +The nRF52840 can be updated on an nRF9160DK using the built-in Segger J-Link programming hardware and the already established methods for flashing (nRF Connect for Desktop Programmer, west, nrfjprog, etc.). + +BLE Devices +----------- +Bluetooth devices running the nRF5SDK and its buttonless secure DFU bootloader can be updated as well. +Create a device group in nRF Cloud for one or more indentical Bluetooth peripherals that are connected to your gateway. +Then in the Updates Dashboard, upload the firmware bundle using the Bundles ... menu, then click Create Update. +Select the device group and the firmware bundle, and click Save. +Then click Apply Update. + +Known Issues and Limitations +**************************** + +Hardware +-------- +1. Battery unplugged from PCB for safe shipping +#. Power button and control is implemented in software; no hard on/off provided +#. SIM card is not accessible from outside the enclosure, though it can be replaced by opening the enclosure on the antenna side +#. JTAG programming of onboard nRF9160 and nRF52840 require special connectors and adapters +#. While there is an U.FL RF connector provided for GPS, there is no mounting hole in the enclosure for an SMA connector and external antenna +#. The cables for the BLE and LTE antennas are easily damaged by the edge of the PCB if they are not routed properly prior to screwing either end cap back on +#. It would be better to have a bigger, more professional-looking waterproof enclosure + +Firmware +-------- +1. Modification and storage of login password by user not yet implemented +#. LTE-M / TLS / MQTT is the only tested configuration (NBIoT and LWM2M not tested and/or implemented) +#. Maximum number of BLE device characteristic notifications per second limited to (approximately 10) +#. BLE Beacons are not supported +#. BLE devices which frequently disconnect and reconnect to save power will not work well if at all +#. Only BLE devices which implement Nordic nRF5 SDK-style Secure DFU can be updated using FOTA +#. FOTA updates of the on-board nRF52840 is not implemented (must be done over USB) +#. Bonding is not yet implemented, so devices which require it will not work well if at all +#. External Flash is not utilized yet to implement full modem updates +#. GPS not supported (single and/or multi cell location services could be supported in the future) + +Cloud +----- +1. Frontend facilities for helping easily provision a gateway using device certificates are not yet released. +#. Frontend graphics are not appropriate for BLE devices or for nRF Cloud BLE Gateway vs. Phone Gateway +#. Bandwidth is used inefficiently by requiring frequent complete rediscovery of BLE device services and characteristics +#. JSON format is overly verbose which wastes gateway SRAM and bandwidth, limiting the number of connected BLE devices and rate of notifications + +Dependencies +************ + +This application uses the following nRF Connect SDK libraries and drivers: + +* `lib_nrf_cloud`_ +* `modem_info_readme`_ +* `at_cmd_parser_readme`_ +* ``lib/modem_lib`` +* `dk_buttons_and_leds_readme`_ +* ``drivers/lte_link_control`` +* ``drivers/flash`` +* ``bluetooth/gatt_dm`` +* ``bluetooth/scan`` + +From Zephyr: + * `zephyr:bluetooth_api`_ + +In addition, it uses the Secure Partition Manager sample: + +* `secure_partition_manager`_ + +For nrf52840: + +* `nrfcloud_gateway_controller`_ +* `hci_lpuart`_ + +History +******* + +The Apricity Gateway application was created using the following nRF Connect SDK sample applications: + + * `lte_ble_gateway`_ + * `asset_tracker`_ + +From Zephyr: + * `zephyr:bluetooth-hci-uart-sample`_ + + +.. ### These are links used in gateway docs. + +.. _PuTTY: https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html + +.. _`Tera Term`: https://ttssh2.osdn.jp/index.html.en + +.. _`LTE Link Monitor`: https://infocenter.nordicsemi.com/topic/ug_link_monitor/UG/link_monitor/lm_intro.html + +.. _`nRF Connect Programmer`: https://infocenter.nordicsemi.com/topic/ug_nc_programmer/UG/nrf_connect_programmer/ncp_introduction.html + +.. _`nRF Connect for Desktop`: https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Connect-for-desktop + +.. _`nRF Cloud`: https://nrfcloud.com/ +.. _`nRF Cloud Provision Devices`: https://nrfcloud.com/#/provision-devices +.. _`nRF Cloud ProvisionDevices REST API`: https://api.nrfcloud.com/v1#operation/ProvisionDevices +.. _`nRF Cloud FetchBulkOpsRequest REST API`: https://api.nrfcloud.com/v1#operation/FetchBulkOpsRequest + +.. _`nrfcloud_gateway_controller`: https://github.com/nRFCloud/lte-gateway-ble + +.. _`Modem Firmware v1.3.0`: https://www.nordicsemi.com/-/media/Software-and-other-downloads/Dev-Kits/nRF9160-DK/nRF9160-modem-FW/mfw_nrf9160_1.3.0.zip + +.. _`nRF91 AT Command Reference`: https://infocenter.nordicsemi.com/topic/ref_at_commands/REF/at_commands/intro.html + +.. _`Create CA Cert`: https://github.com/nRFCloud/utils/tree/master/python/modem-firmware-1.3%2B#create-ca-cert + +.. _`Device Credentials Installer`: https://github.com/nRFCloud/utils/tree/master/python/modem-firmware-1.3%2B#device-credentials-installer + +.. _`asset_tracker`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/applications/asset_tracker/README.html +.. _`lte_ble_gateway`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/samples/nrf9160/lte_ble_gateway/README.html +.. _`lib_nrf_cloud_fota`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/libraries/networking/nrf_cloud.html#firmware-over-the-air-fota-updates +.. _`nRF9160 DK `: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/ug_nrf9160.html#ug-nrf9160 +.. _`hci_lpuart`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/samples/bluetooth/hci_lpuart/README.html +.. _`secure_partition_manager`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/samples/spm/README.html#secure-partition-manager +.. _`lib_nrf_cloud`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/libraries/networking/nrf_cloud.html +.. _`modem_info_readme`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/libraries/modem/modem_info.html +.. _`at_cmd_parser_readme`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/libraries/modem/at_cmd_parser.html +.. _`dk_buttons_and_leds_readme`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/libraries/others/dk_buttons_and_leds.html + +.. _`zephyr:bluetooth_api`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr/reference/bluetooth/index.html +.. _`zephyr:bluetooth-hci-uart-sample`: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr/samples/bluetooth/hci_uart/README.html diff --git a/samples/cellular/nrf_cloud_multi_service/apricity_gateway_nrf9160_ns.overlay b/samples/cellular/nrf_cloud_multi_service/apricity_gateway_nrf9160_ns.overlay new file mode 100644 index 000000000000..194557241a0a --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/apricity_gateway_nrf9160_ns.overlay @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/ { + chosen { + zephyr,bt-uart=&uart1; + }; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/boards/apricity_gateway_nrf9160_ns.conf b/samples/cellular/nrf_cloud_multi_service/boards/apricity_gateway_nrf9160_ns.conf new file mode 100644 index 000000000000..057fb3d976db --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/apricity_gateway_nrf9160_ns.conf @@ -0,0 +1,8 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# Apricity +CONFIG_ENTER_52840_MCUBOOT_VIA_BUTTON=y diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/CMakeLists.txt b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/CMakeLists.txt new file mode 100644 index 000000000000..fd96983f872e --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/CMakeLists.txt @@ -0,0 +1,8 @@ +# Kconfig - APRICITY GATEWAY NRF9160 board configuration +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +zephyr_library() +zephyr_library_sources(nrf52840_reset.c) diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig new file mode 100644 index 000000000000..eedfc09b2a47 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig @@ -0,0 +1,18 @@ +# Apricity Gateway nRF9160 board configuration +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +module=BOARD +module-dep=LOG +module-str=Log level for board +source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config" + +config BOARD_NRF52840_GPIO_RESET + bool "Use nRF52840 PCA20035 GPIO reset pin" + default y + help + Use a GPIO pin to reset the nRF52840 controller and let it wait + until all bytes traveling to the H4 device have been received + and drained, thus ensuring communication can begin correctly. diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.board b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.board new file mode 100644 index 000000000000..0b3c8207e1a6 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.board @@ -0,0 +1,15 @@ +# Apricity Gateway nRF9160 board configuration +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +if SOC_NRF9160_SICA + +config BOARD_APRICITY_GATEWAY_NRF9160 + bool "nRF9160 APRICITY GATEWAY" + +config BOARD_APRICITY_GATEWAY_NRF9160_NS + bool "nRF9160 APRICITY GATEWAY non-secure" + +endif # SOC_NRF9160_SICA diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.defconfig b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.defconfig new file mode 100644 index 000000000000..af8da70dae3c --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.defconfig @@ -0,0 +1,60 @@ +# Apricity Gateway nRF9160 board configuration +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +if BOARD_APRICITY_GATEWAY_NRF9160 || BOARD_APRICITY_GATEWAY_NRF9160_NS + +config BOARD + default "apricity_gateway_nrf9160" + +# By default, if we build for a Non-Secure version of the board, +# enable building with TF-M as the Secure Execution Environment. +config BUILD_WITH_TFM + default y if BOARD_NRF9160DK_NRF9160_NS + +if BUILD_WITH_TFM + +# By default, if we build with TF-M, instruct build system to +# flash the combined TF-M (Secure) & Zephyr (Non Secure) image +config TFM_FLASH_MERGED_BINARY + bool + default y + +endif # BUILD_WITH_TFM + +# For the secure version of the board the firmware is linked at the beginning +# of the flash, or into the code-partition defined in DT if it is intended to +# be loaded by MCUboot. If the secure firmware is to be combined with a non- +# secure image (TRUSTED_EXECUTION_SECURE=y), the secure FW image shall always +# be restricted to the size of its code partition. +# For the non-secure version of the board, the firmware +# must be linked into the code-partition (non-secure) defined in DT, regardless. +# Apply this configuration below by setting the Kconfig symbols used by +# the linker according to the information extracted from DT partitions. + +config FLASH_LOAD_SIZE + default $(dt_chosen_reg_size_hex,$(DT_CHOSEN_Z_CODE_PARTITION)) + depends on BOARD_APRICITY_GATEWAY_NRF9160 && TRUSTED_EXECUTION_SECURE + +if BOARD_APRICITY_GATEWAY_NRF9160_NS + +config FLASH_LOAD_OFFSET + default $(dt_chosen_reg_addr_hex,$(DT_CHOSEN_Z_CODE_PARTITION)) + +config FLASH_LOAD_SIZE + default $(dt_chosen_reg_size_hex,$(DT_CHOSEN_Z_CODE_PARTITION)) + +endif # BOARD_APRICITY_GATEWAY_NRF9160_NS + +config BT_HCI_VS + default y if BT + +config BT_WAIT_NOP + default BT && $(dt_nodelabel_enabled,nrf52840_reset) + +config I2C + default $(dt_compat_on_bus,$(DT_COMPAT_NXP_PCAL6408A),i2c) + +endif # BOARD_APRICITY_GATEWAY_NRF9160 || BOARD_APRICITY_GATEWAY_NRF9160_NS diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.sysbuild b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.sysbuild new file mode 100644 index 000000000000..d969caac9f7a --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/Kconfig.sysbuild @@ -0,0 +1,17 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +choice BOOTLOADER + default BOOTLOADER_MCUBOOT +endchoice + +menu "Thingy91 configuration" + +if BOARD_APRICITY_GATEWAY_NRF9160_NS + +endif # if BOARD_APRICITY_GATEWAY_NRF9160_NS + +endmenu diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160.dts b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160.dts new file mode 100644 index 000000000000..b9724ab24b0c --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160.dts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/dts-v1/; +#include +#include "apricity_gateway_nrf9160_common.dts" + +/ { + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + }; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160.yaml b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160.yaml new file mode 100644 index 000000000000..595d702ab954 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160.yaml @@ -0,0 +1,16 @@ +identifier: apricity_gateway_nrf9160 +name: Apricity-Gateway-NRF9160 +type: mcu +arch: arm +toolchain: + - gnuarmemb + - xtools + - zephyr +ram: 88 +flash: 1024 +supported: + - gpio + - i2c + - pwm + - spi + - counter diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_common.dts b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_common.dts new file mode 100644 index 000000000000..d36ad93eff12 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_common.dts @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/ { + model = "Apricity Gateway nRF9160"; + compatible = "apricity,apricity-gateway-nrf9160"; + + chosen { + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-hci=&bt_hci_uart; + }; + + buttons { + compatible = "gpio-keys"; + + button0: button_0 { + gpios = <&gpio0 17 0>; + label = "Button 1"; + }; + }; + + leds { + compatible = "gpio-leds"; + red_led: led_1 { + gpios = <&gpio0 29 0>; + label = "RGB red channel"; + }; + green_led: led_2 { + gpios = <&gpio0 30 0>; + label = "RGB green channel"; + }; + blue_led: led_3 { + gpios = <&gpio0 31 0>; + label = "RGB blue channel"; + }; + red_led_2: led_4 { + gpios = <&gpio0 26 0>; + label = "RGB red 2 channel"; + }; + green_led_2: led_5 { + gpios = <&gpio0 27 0>; + label = "RGB green 2 channel"; + }; + blue_led_2: led_6 { + gpios = <&gpio0 28 0>; + label = "RGB blue 2 channel"; + }; + }; + + interface_to_nrf52840: gpio-interface { + compatible = "nordic,nrf9160dk-nrf52840-interface"; + #gpio-cells = <2>; + gpio-map-mask = <0xf 0>; + gpio-map-pass-thru = <0 0xffffffff>; + gpio-map = <0 0 &gpio0 18 0>, + <1 0 &gpio0 19 0>, + <2 0 &gpio0 20 0>, + <3 0 &gpio0 21 0>, + <4 0 &gpio0 22 0>, + <5 0 &gpio0 23 0>, + /* 6: COEX0 */ + /* 7: COEX1 */ + /* 8: COEX2 */ + <9 0 &gpio0 13 0>, + <10 0 &gpio0 5 0>, + <11 0 &gpio0 9 0>; + }; + + nrf52840_reset: gpio-reset { + compatible = "nordic,nrf9160dk-nrf52840-reset"; + status = "okay"; + /* + * This line is specified as active high for compatibility + * with the previously used Kconfig-based configuration. + */ + gpios = <&interface_to_nrf52840 9 GPIO_ACTIVE_HIGH>; + }; + + nrf52840_boot: gpio-boot { + compatible = "nordic,nrf9160dk-nrf52840-boot"; + status = "okay"; + gpios = <&interface_to_nrf52840 10 GPIO_PULL_UP>; + }; + + ext_mem_ctrl: gpio-ext-mem-ctrl { + compatible = "nordic,ext-mem-ctrl"; + status = "okay"; + gpios = <&interface_to_nrf52840 11 0>; + }; + + aliases { + sw0 = &button0; + led0 = &red_led; + led1 = &green_led; + led2 = &blue_led; + led0-1 = &red_led_2; + led1-1 = &green_led_2; + led2-1 = &blue_led_2; + rgb-pwm = &pwm0; + rgb2-pwm = &pwm1; + }; +}; + +&adc { + status = "okay"; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +/* PWM0 is intended for RGB LED control */ +&pwm0 { + compatible = "nordic,nrf-pwm"; + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* PWM1 is intended for RGB LED control */ +&pwm1 { + compatible = "nordic,nrf-pwm"; + status = "okay"; + pinctrl-0 = <&pwm1_default>; + pinctrl-1 = <&pwm1_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi3_default_alt>; + pinctrl-1 = <&spi3_sleep_alt>; + pinctrl-names = "default", "sleep"; + cs-gpios = <&gpio0 8 1>; + w25q64jv: w25q64jv@0 { + status = "okay"; + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <80000000>; + jedec-id = [ef 40 17]; + size = <67108864>; + }; +}; + +&pinctrl { + spi3_default_alt: spi3_default_alt { + group1 { + psels = , + , + ; + nordic,drive-mode = ; + }; + }; + + spi3_sleep_alt: spi3_sleep_alt { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + pwm0_default: pwm0_default { + group1 { + psels = , + , + ; + nordic,invert; + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = , + , + ; + low-power-enable; + nordic,invert; + }; + }; + + pwm1_default: pwm1_default { + group1 { + psels = , + , + ; + nordic,invert; + }; + }; + + pwm1_sleep: pwm1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + nordic,invert; + }; + }; + + uart0_default: uart0_default { + group1 { + psels = , + , + , + ; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + , + , + ; + low-power-enable; + }; + }; + + uart1_default: uart1_default { + group1 { + psels = , + , + , + ; + }; + }; + + uart1_sleep: uart1_sleep { + group1 { + psels = , + , + , + ; + low-power-enable; + }; + }; +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + current-speed = <115200>; + status = "okay"; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&uart1 { + compatible = "nordic,nrf-uarte"; + current-speed = <1000000>; + status = "okay"; + hw-flow-control; + pinctrl-0 = <&uart1_default>; + pinctrl-1 = <&uart1_sleep>; + pinctrl-names = "default", "sleep"; + bt_hci_uart: bt_hci_uart { + compatible = "zephyr,bt-hci-uart"; + status = "okay"; + }; +}; + +&flash0 { + /* + * For more information, see: + * http://docs.zephyrproject.org/devices/dts/flash_partitions.html + */ + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x10000>; + }; + slot0_partition: partition@10000 { + label = "image-0"; + }; + slot0_ns_partition: partition@50000 { + label = "image-0-nonsecure"; + }; + slot1_partition: partition@80000 { + label = "image-1"; + }; + slot1_ns_partition: partition@c0000 { + label = "image-1-nonsecure"; + }; + scratch_partition: partition@f0000 { + label = "image-scratch"; + reg = <0x000f0000 0xa000>; + }; + /* 0xf0000 to 0xf7fff reserved for TF-M partitions */ + storage_partition: partition@fa000 { + label = "storage"; + reg = <0x000f8000 0x00008000>; + }; + }; +}; + +/ { + + reserved-memory { + #address-cells = <1>; + #size-cells = <1>; + ranges; + + sram0_s: image_s@20000000 { + /* Secure image memory */ + }; + + sram0_bsd: image_modem@20010000 { + /* Modem (shared) memory */ + }; + + sram0_ns: image_ns@20020000 { + /* Non-Secure image memory */ + }; + }; +}; + +/* Include partition configuration file */ +#include "apricity_gateway_nrf9160_partition_conf.dts" diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_defconfig b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_defconfig new file mode 100644 index 000000000000..0c1655816494 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_defconfig @@ -0,0 +1,28 @@ +CONFIG_SOC_SERIES_NRF91X=y +CONFIG_SOC_NRF9160_SICA=y +CONFIG_BOARD_APRICITY_GATEWAY_NRF9160=y + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y + +# Enable TrustZone-M +CONFIG_ARM_TRUSTZONE_M=y + +# Enable PINCTRL +CONFIG_PINCTRL=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable UARTE +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# Disable entropy driver, as it's not yet implemented for nRF9160 +CONFIG_ENTROPY_NRF5_RNG=n diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns.dts b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns.dts new file mode 100644 index 000000000000..a74979467523 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns.dts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/dts-v1/; +#include +#include "apricity_gateway_nrf9160_common.dts" + +/ { + chosen { + zephyr,flash = &flash0; + zephyr,sram = &sram0_ns; + zephyr,code-partition = &slot0_ns_partition; + }; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns.yaml b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns.yaml new file mode 100644 index 000000000000..0b6839722f22 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns.yaml @@ -0,0 +1,16 @@ +identifier: apricity_gateway_nrf9160_ns +name: APRICITY_GATEWAY-nRF9160-Non-Secure +type: mcu +arch: arm +toolchain: + - gnuarmemb + - zephyr +ram: 128 +flash: 256 +supported: + - serial + - spi + - i2c + - pwm + - netif:modem + - gpio diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns_defconfig b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns_defconfig new file mode 100644 index 000000000000..bd35a4c5c36c --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_ns_defconfig @@ -0,0 +1,39 @@ +CONFIG_SOC_SERIES_NRF91X=y +CONFIG_SOC_NRF9160_SICA=y +CONFIG_BOARD_APRICITY_GATEWAY_NRF9160_NS=y + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y + +# Enable TrustZone-M +CONFIG_ARM_TRUSTZONE_M=y + +# This Board implies building Non-Secure firmware +CONFIG_TRUSTED_EXECUTION_NONSECURE=y + +# Enable PINCTRL +CONFIG_PINCTRL=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable UARTE +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# Disable entropy driver, as it's not yet implemented for nRF9160 +CONFIG_ENTROPY_NRF5_RNG=n + +# Enable SPI +CONFIG_SPI=y + +# Disable entropy driver, as it's not yet implemented for nRF9160 +CONFIG_ENTROPY_NRF5_RNG=n + +CONFIG_BOOTLOADER_MCUBOOT=y diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_partition_conf.dts b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_partition_conf.dts new file mode 100644 index 000000000000..505b07fc6028 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/apricity_gateway_nrf9160_partition_conf.dts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* + * Default Flash planning for apricity_gateway_nrf9160. + * + * Zephyr build for nRF9160 with ARM TrustZone-M support, + * implies building Secure and Non-Secure Zephyr images. + * + * Secure image will be placed, by default, in flash0 + * (or in slot0, if MCUboot is present). + * Secure image will use sram0 for system memory. + * + * Non-Secure image will be placed in slot0_ns, and use + * sram0_ns for system memory. + * + * Note that the Secure image only requires knowledge of + * the beginning of the Non-Secure image (not its size). + */ + +&slot0_partition { + reg = <0x00010000 0x40000>; +}; + +&slot0_ns_partition { + reg = <0x00050000 0x30000>; +}; + +&slot1_partition { + reg = <0x00080000 0x40000>; +}; + +&slot1_ns_partition { + reg = <0x000c0000 0x30000>; +}; + +/* Default SRAM planning when building for nRF9160 with + * ARM TrustZone-M support + * - Lowest 88 kB SRAM allocated to Secure image (sram0_s). + * - 40 kB SRAM reserved for and used by the modem library + * (sram0_modem). This memory is Non-Secure. + * - Upper 128 kB allocated to Non-Secure image (sram0_ns). + * When building with TF-M, both sram0_modem and sram0_ns + * are allocated to the Non-Secure image. + */ + +&sram0_s { + reg = <0x20000000 DT_SIZE_K(88)>; +}; + +&sram0_bsd { + reg = <0x20016000 DT_SIZE_K(40)>; +}; + +&sram0_ns { + reg = <0x20020000 DT_SIZE_K(128)>; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/board.cmake b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/board.cmake new file mode 100644 index 000000000000..3452af649bcc --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/board.cmake @@ -0,0 +1,11 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA. +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +if(CONFIG_TFM_FLASH_MERGED_BINARY) + set_property(TARGET runners_yaml_props_target PROPERTY hex_file tfm_merged.hex) +endif() + +board_runner_args(jlink "--device=cortex-m33" "--speed=4000") +board_runner_args(nrfjprog "--softreset") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-boot.yaml b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-boot.yaml new file mode 100644 index 000000000000..82b332dc562e --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-boot.yaml @@ -0,0 +1,21 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +# NOTE: This file is replicated in nrf9160dk_nrf9160 and nrf9160dk_nrf52840. +# Any changes should be done in both instances. + +description: GPIO used to control boot mode for nRF52840 on nRF9160 DK + +compatible: "nordic,nrf9160dk-nrf52840-boot" + +include: base.yaml + +properties: + status: + required: true + + gpios: + type: phandle-array + required: true + description: | + GPIO to use as nRF52840 boot mode line: output in nRF9160, input in nRF52840. diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-interface.yaml b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-interface.yaml new file mode 100644 index 000000000000..5a841580581a --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-interface.yaml @@ -0,0 +1,42 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +# NOTE: This file is replicated in nrf9160dk_nrf9160 and nrf9160dk_nrf52840. +# Any changes should be done in both instances. + +description: | + nRF9160 DK GPIO interface between nRF9160 and nRF52840 + + This interface can be used for inter-SoC communication on the DK. + The connections are as follows: + + | nRF9160 | | nRF52840 | + | P0.17 | -- nRF interface line 0 -- | P0.17 | + | P0.18 | -- nRF interface line 1 -- | P0.20 | + | P0.19 | -- nRF interface line 2 -- | P0.15 | + | P0.21 | -- nRF interface line 3 -- | P0.22 | + | P0.22 | -- nRF interface line 4 -- | P1.04 | + | P0.23 | -- nRF interface line 5 -- | P1.02 | + | COEX0 | -- nRF interface line 6 -- | P1.13 | + | COEX1 | -- nRF interface line 7 -- | P1.11 | + | COEX2 | -- nRF interface line 8 -- | P1.15 | + | P0.24 | -- nRF interface line 9 -- | P0.18 (nRESET) | (in v0.14.0 or later) + + Before particular lines of this interface can be used, the corresponding + analog switches that control the routing of involved nRF9160 pins must be + configured to provide the optional routing (i.e. to nRF52840). To achieve + this, set the status of respective devicetree nodes in the firmware for + the nrf9160dk_nrf52840 board to "okay": + - `nrf_interface_pins_0_2_routing` to enable lines 0-2 + - `nrf_interface_pins_3_5_routing` to enable lines 3-5 + - `nrf_interface_pins_6_8_routing` to enable lines 6-8 + - `nrf_interface_pin_9_routing` to enable line 9 (this line is only + available in nRF9160 DK v0.14.0 or later) + + NOTE: In nRF9160 DK revisions earlier than v0.14.0, when the above signals + from nRF9160 are routed to nRF52840, they are not available on the DK + connectors. + +compatible: "nordic,nrf9160dk-nrf52840-interface" + +include: [gpio-nexus.yaml, base.yaml] diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-reset.yaml b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-reset.yaml new file mode 100644 index 000000000000..22ee6a22529f --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/dts/bindings/nordic,nrf9160dk-nrf52840-reset.yaml @@ -0,0 +1,21 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +# NOTE: This file is replicated in nrf9160dk_nrf9160 and nrf9160dk_nrf52840. +# Any changes should be done in both instances. + +description: GPIO used to reset nRF52840 on nRF9160 DK + +compatible: "nordic,nrf9160dk-nrf52840-reset" + +include: base.yaml + +properties: + status: + required: true + + gpios: + type: phandle-array + required: true + description: | + GPIO to use as nRF52840 reset line: output in nRF9160, input in nRF52840. diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/nrf52840_reset.c b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/nrf52840_reset.c new file mode 100644 index 000000000000..75bd8f3038c9 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/nrf52840_reset.c @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA. + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include + +#define RESET_NODE DT_NODELABEL(nrf52840_reset) +#define BOOT_NODE DT_NODELABEL(nrf52840_boot) + +#if DT_NODE_HAS_STATUS(RESET_NODE, okay) && DT_NODE_HAS_STATUS(BOOT_NODE, okay) + +#define RESET_GPIO_CTRL DT_GPIO_CTLR(RESET_NODE, gpios) +#define RESET_GPIO_PIN DT_GPIO_PIN(RESET_NODE, gpios) +#define RESET_GPIO_FLAGS DT_GPIO_FLAGS(RESET_NODE, gpios) + +#define BOOT_GPIO_CTRL DT_GPIO_CTLR(BOOT_NODE, gpios) +#define BOOT_GPIO_PIN DT_GPIO_PIN(BOOT_NODE, gpios) +#define BOOT_GPIO_FLAGS DT_GPIO_FLAGS(BOOT_NODE, gpios) + +#define WAIT_BOOT_INTERVAL_MS 50 + +static int nrf52840_reset_assert(bool boot_select) +{ + const struct device *reset_port = DEVICE_DT_GET(RESET_GPIO_CTRL); + const struct device *boot_port = DEVICE_DT_GET(BOOT_GPIO_CTRL); + int err; + + if (!reset_port) { + printk("reset_port not found!\n"); + return -EIO; + } + + if (!boot_port) { + printk("boot_port not found!\n"); + return -EIO; + } + + if (!device_is_ready(reset_port)) { + printk("reset_port is not ready!\n"); + return -EIO; + } + + if (!device_is_ready(boot_port)) { + printk("boot_port is not ready!\n"); + return -EIO; + } + + /* Configure pin as output and initialize it to low. */ + err = gpio_pin_configure(reset_port, RESET_GPIO_PIN, GPIO_OUTPUT_LOW); + if (err) { + printk("Reset pin could not be configured! %d\n", err); + return err; + } + + if (boot_select) { + printk("Resetting nrf52840 in MCUboot USB serial update mode\n"); + /* delay to ensure logging finishes before reset */ + k_sleep(K_SECONDS(2)); + } else { + printk("Resetting nrf52840 for normal ops.\n"); + } + err = gpio_pin_configure(boot_port, BOOT_GPIO_PIN, GPIO_OUTPUT | + GPIO_OPEN_DRAIN | GPIO_PULL_UP); + if (err) { + printk("Boot pin could not be configured! %d\n", err); + return err; + } + + err = gpio_pin_set(boot_port, BOOT_GPIO_PIN, boot_select ? 0 : 1); + if (err) { + printk("Boot pin could not be set! %d\n", err); + return err; + } + + err = gpio_pin_set(reset_port, RESET_GPIO_PIN, 1); + if (err) { + printk("Reset pin could not be reset! %d\n", err); + return err; + } + printk("Reset the 52840 with boot set to %d\n", boot_select); + return 0; +} + +static int nrf52840_boot_select_deassert(void) +{ + const struct device *boot_port = DEVICE_DT_GET(BOOT_GPIO_CTRL); + int err; + + if (!boot_port) { + printk("boot_port not found!\n"); + return -EIO; + } + + if (!device_is_ready(boot_port)) { + printk("boot_port is not ready!\n"); + return -EIO; + } + + err = gpio_pin_set(boot_port, BOOT_GPIO_PIN, 1); + if (err) { + printk("Boot pin could not be set! %d\n", err); + return err; + } + + k_sleep(K_MSEC(10)); + + err = gpio_pin_configure(boot_port, BOOT_GPIO_PIN, + GPIO_INPUT | GPIO_PULL_UP); + if (err) { + printk("Boot pin could not be configured! %d\n", err); + return err; + } + printk("Deasserted 52840 boot pin\n"); + return 0; +} + +static int nrf52840_reset_deassert(void) +{ + const struct device *reset_port = DEVICE_DT_GET(RESET_GPIO_CTRL); + int err; + + if (!reset_port) { + printk("reset_port not found!\n"); + return -EIO; + } + + if (!device_is_ready(reset_port)) { + printk("reset_port is not ready!\n"); + return -EIO; + } + + err = gpio_pin_set(reset_port, RESET_GPIO_PIN, 0); + if (err) { + printk("Reset pin could not be reset! %d\n", err); + return err; + } + printk("Deasserted 52840 reset\n"); + return 0; +} + +int nrf52840_wait_boot_complete(int timeout_ms) +{ + const struct device *boot_port = DEVICE_DT_GET(BOOT_GPIO_CTRL); + int total_time; + int err; + + if (!boot_port) { + printk("boot_port not found!\n"); + return -EIO; + } + + if (!device_is_ready(boot_port)) { + printk("boot_port is not ready!\n"); + return -EIO; + } + + /* make the boot select pin a pulled up input, so we can + * read it to see when the 52840 has rebooted into its application + */ + err = gpio_pin_configure(boot_port, BOOT_GPIO_PIN, GPIO_INPUT | GPIO_PULL_UP); + if (err) { + printk("Boot pin could not be configured! %d\n", err); + return err; + } + + printk("Waiting for 52840 to boot...\n"); + total_time = 0; + do { + k_sleep(K_MSEC(WAIT_BOOT_INTERVAL_MS)); + total_time += WAIT_BOOT_INTERVAL_MS; + err = gpio_pin_get_raw(boot_port, BOOT_GPIO_PIN); + if (err < 0) { + return err; + } + if (err && (timeout_ms > 0) && (total_time > timeout_ms)) { + return -ETIMEDOUT; + } + } while (err == 1); + + printk("52840 boot is complete\n"); + return 0; +} + +int nrf52840_reset_to_mcuboot(void) +{ + int err; + + printk("Reset 52840 into MCUboot mode:\n"); + err = nrf52840_reset_assert(true); + if (err) { + return err; + } + k_sleep(K_MSEC(10)); + + err = nrf52840_reset_deassert(); + if (err) { + return err; + } + + k_sleep(K_SECONDS(5)); + return nrf52840_boot_select_deassert(); +} + +int bt_hci_transport_setup(struct device *h4) +{ + char c; + int err; + + /* Reset the nRF52840 and let it wait until the pin is + * pulled low again before running to main to ensure + * that it won't send any data until the H4 device + * is setup and ready to receive. + */ + err = nrf52840_reset_assert(false); + if (err) { + return err; + } + + /* Wait for the nRF52840 peripheral to stop sending data. + * + * It is critical (!) to wait here, so that all bytes + * on the lines are received and drained correctly. + */ + k_sleep(K_MSEC(10)); + + /* Drain bytes */ + while (h4 && uart_fifo_read(h4, &c, 1)) { + continue; + } + + /* We are ready, let the nRF52840 run to main */ + nrf52840_reset_deassert(); + + return 0; +} +#else +#warning "Reset and/or boot node is missing" +#endif /* DT_NODE_HAS_STATUS(RESET_NODE, okay) && DT_NODE_HAS_STATUS(BOOT_NODE, okay) */ diff --git a/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/pre_dt_board.cmake b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/pre_dt_board.cmake new file mode 100644 index 000000000000..4959e498ccfe --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/arm/apricity_gateway_nrf9160/pre_dt_board.cmake @@ -0,0 +1,7 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +# Suppress "unique_unit_address_if_enabled" to handle the following overlaps: +# - flash-controller@39000 & kmu@39000 +# - power@5000 & clock@5000 +list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled") diff --git a/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_0_14_0.overlay b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_0_14_0.overlay new file mode 100644 index 000000000000..6d30ec7279d1 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_0_14_0.overlay @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ +/* + * #include + * #include + * #include + */ +#include + +&nrf52840_reset { + status = "okay"; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns.conf b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns.conf new file mode 100644 index 000000000000..b3544793546d --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns.conf @@ -0,0 +1,18 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# 9160DK +#CONFIG_NRF_SW_LPUART=y +#CONFIG_NRF_SW_LPUART_INT_DRIVEN=y + +# UART_2/LPUART for use with nrf/samples/bluetooth/hci_lpuart on 52840 +#CONFIG_UART_2_ASYNC=y +CONFIG_UART_2_INTERRUPT_DRIVEN=y +#CONFIG_UART_2_NRF_HW_ASYNC=y +#CONFIG_UART_2_NRF_HW_ASYNC_TIMER=2 + +#CONFIG_NRF_CLOUD_HOST_NAME="mqtt.beta.nrfcloud.com" +#CONFIG_NRF_CLOUD_SEC_TAG=44 diff --git a/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns.overlay b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns.overlay new file mode 100644 index 000000000000..eaa0e8ac2cd6 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns.overlay @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ +/* Use the reset line that is available until v0.14.0 of the DK. */ +#include + +/ { + chosen { + zephyr,bt-hci=&bt_hci_uart; + }; +}; + +&gpiote { + interrupts = <49 NRF_DEFAULT_IRQ_PRIORITY>; +}; + +&uart2 { + current-speed = <1000000>; + status = "okay"; + hw-flow-control; + + pinctrl-0 = <&uart2_default_alt>; + pinctrl-1 = <&uart2_sleep_alt>; + pinctrl-names = "default", "sleep"; + bt_hci_uart: bt_hci_uart { + compatible = "zephyr,bt-hci-uart"; + status = "okay"; + }; +}; + +&nrf52840_reset { + status = "okay"; +}; + +&pinctrl { + uart2_default_alt: uart2_default_alt { + group1 { + psels = , + ; + }; + group2 { + psels = , + ; + bias-pull-up; + }; + }; + + uart2_sleep_alt: uart2_sleep_alt { + group1 { + psels = , + , + , + ; + low-power-enable; + }; + }; + +}; diff --git a/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns_0_14_0.overlay b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns_0_14_0.overlay index 852486ce8feb..00352ac3d37d 100644 --- a/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns_0_14_0.overlay +++ b/samples/cellular/nrf_cloud_multi_service/boards/nrf9160dk_nrf9160_ns_0_14_0.overlay @@ -1,8 +1,10 @@ /* - * Copyright (c) 2022 Nordic Semiconductor ASA + * Copyright (c) 2024 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause */ +/* Use the reset line that is available starting from v0.14.0 of the DK. */ +#include / { chosen { @@ -14,3 +16,10 @@ &mx25r64 { status = "okay"; }; + +/* Enable high drive mode for the SPI3 pins to get a square signal at 8 MHz */ +&spi3_default { + group1 { + nordic,drive-mode = ; + }; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/overlay_ble_gateway.conf b/samples/cellular/nrf_cloud_multi_service/overlay_ble_gateway.conf new file mode 100644 index 000000000000..632dd788f49a --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/overlay_ble_gateway.conf @@ -0,0 +1,151 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# Log level +# For more verbose and detailed log output, set the log level to +# CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL_DBG=y instead. +CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL_DBG=y + +# CONFIG_LOG_BUFFER_SIZE is the aforementioned buffer. A size of 12000 bytes is useful for debugging +# and prototyping, but is probably more than necessary for production-ready firmware. +CONFIG_LOG_BUFFER_SIZE=12000 +CONFIG_LOG_SPEED=y +CONFIG_LOG_FUNC_NAME_PREFIX_INF=y +CONFIG_LOG_FUNC_NAME_PREFIX_ERR=y +CONFIG_LOG_PRINTK=y + +# Heap and stacks +# Extended memory heap size needed both for PGPS and for encoding JSON-based nRF Cloud Device Messages. +CONFIG_HEAP_MEM_POOL_SIZE=20480 +CONFIG_MAIN_STACK_SIZE=3072 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 +# The default modem shared memory buffer for TX is significantly larger than required. +# RX buffer has to be larger because the sample reads certificates so using default value. +CONFIG_NRF_MODEM_LIB_SHMEM_TX_SIZE=4096 +CONFIG_HW_STACK_PROTECTION=y +CONFIG_EXTRA_EXCEPTION_INFO=y + +# Enable power savings mode +CONFIG_LTE_PSM_REQ=n +# Set the PSM Requested Active Time to 20 seconds +#CONFIG_LTE_PSM_REQ_RAT="00001010" + +# not enough flash to enable these currently: +CONFIG_SECURE_BOOT=n +CONFIG_BUILD_S1_VARIANT=n + +# Services configuration +CONFIG_TEMP_TRACKING=n +CONFIG_LOCATION_TRACKING=n +CONFIG_LOCATION=n +CONFIG_LOCATION_METHOD_GNSS=n +CONFIG_LOCATION_METHOD_CELLULAR=n +CONFIG_NRF_CLOUD_AGNSS=n +CONFIG_NRF_CLOUD_LOCATION=n +CONFIG_MODEM_INFO_ADD_SIM=y +CONFIG_NRF_CLOUD_PGPS=n +CONFIG_NRF_CLOUD_PGPS_REQUEST_UPON_INIT=n +CONFIG_NRF_CLOUD_MQTT_SHADOW_TRANSFORMS=y +CONFIG_NRF_CLOUD_ALERT=n +CONFIG_NRF_CLOUD_LOG_DIRECT=n +CONFIG_NRF_CLOUD_LOG_OUTPUT_LEVEL=3 +CONFIG_NRF_CLOUD_SEND_SERVICE_INFO_UI=n + +# Instrument the heap +CONFIG_MEM_SLAB_TRACE_MAX_UTILIZATION=y +CONFIG_SYS_HEAP_RUNTIME_STATS=y + +CONFIG_GATEWAY_BLE_FOTA=n + +# Enable Bluetooth stack and libraries +#DT_HAS_ZEPHYR_BT_HCI_UART_ENABLED && BT_HCI && BT_DRIVERS + +CONFIG_BT=y +CONFIG_BT_H4=y +CONFIG_BT_HCI=y +CONFIG_BT_CTLR=n +CONFIG_BT_CENTRAL=y +CONFIG_BT_DRIVERS=y +CONFIG_BT_WAIT_NOP=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_GATT_DM=y +CONFIG_BT_SCAN=y +CONFIG_BT_SCAN_FILTER_ENABLE=y +CONFIG_BT_SCAN_UUID_CNT=1 +CONFIG_BT_SCAN_NAME_CNT=0 +CONFIG_BT_MAX_CONN=8 +CONFIG_BT_BUF_ACL_RX_COUNT_EXTRA=1 +CONFIG_BT_GATT_DM_MAX_ATTRS=100 +CONFIG_BT_FILTER_ACCEPT_LIST=y +CONFIG_BT_SETTINGS=n +CONFIG_BT_EXT_ADV=y +CONFIG_BT_HCI_VS=y +CONFIG_BT_REMOTE_VERSION=y + +#CONFIG_BT_DEBUG_LOG=y +#CONFIG_BT_DEBUG_CONN=y +#CONFIG_BT_DEBUG_GATT=n +#CONFIG_BT_DEBUG_HCI_CORE=y +#CONFIG_BT_DEBUG_HCI_DRIVER=y +CONFIG_BT_LOG_LEVEL_INF=y + +CONFIG_BT_HCI_TX_STACK_SIZE=2048 +CONFIG_BT_HCI_TX_STACK_SIZE_WITH_PROMPT=y +CONFIG_BT_RX_STACK_SIZE=2048 + +# Gateway +CONFIG_NRF_CLOUD_GATEWAY=y +CONFIG_FLASH_TEST=n +CONFIG_AT_CMD_REQUESTS=n + +# for customer builds, set below to n +CONFIG_GATEWAY_DBG_CMDS=y + +# to enable AT command handling unfettered by the shell, turn the below +# option on, and CONFIG_GATEWAY_SHELL off (ignore warnings about the symbols +# in the block below that, or comment them out) +CONFIG_AT_HOST_LIBRARY=n +CONFIG_GATEWAY_SHELL=y + +# set various buffer sizes in shell +CONFIG_SHELL_STACK_SIZE=6144 +CONFIG_SHELL_CMD_BUFF_SIZE=3072 +CONFIG_SHELL_PRINTF_BUFF_SIZE=4096 +CONFIG_SHELL_BACKEND_SERIAL_API_INTERRUPT_DRIVEN=y +CONFIG_SHELL_BACKEND_SERIAL_RX_RING_BUFFER_SIZE=3072 +CONFIG_SHELL_PROMPT_UART="login: " +CONFIG_SHELL_START_OBSCURED=n +CONFIG_SHELL_PROMPT_SECURE="gateway:# " + +# turn on for low level BLE commands +CONFIG_BT_SHELL=n + +# force these off -- they default on +CONFIG_LOG_CMDS=n +CONFIG_PWM_SHELL=n +CONFIG_BOOT_BANNER=n +CONFIG_FLASH_SHELL=n +CONFIG_DEVICE_SHELL=n +CONFIG_DEVMEM_SHELL=n +#CONFIG_SHELL_CMDS=n +CONFIG_SHELL_WILDCARD=n +CONFIG_SHELL_CMDS_RESIZE=y +CONFIG_CLOCK_CONTROL_NRF_SHELL=n + +CONFIG_DEBUG_COREDUMP=n +#CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y +#CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y + +# change logging level when at login prompt +CONFIG_STARTING_LOG_OVERRIDE=n +CONFIG_STARTING_LOG_LEVEL_INF=y +CONFIG_NRF_CLOUD_LOG_LEVEL_INF=y +CONFIG_NET_LOG=y +CONFIG_MQTT_LOG_LEVEL_INF=y + +CONFIG_DEBUG_OPTIMIZATIONS=y +CONFIG_DEBUG_THREAD_INFO=y +CONFIG_THREAD_MONITOR=y diff --git a/samples/cellular/nrf_cloud_multi_service/src/application.c b/samples/cellular/nrf_cloud_multi_service/src/application.c index 0eacadab7c85..f6f17257836a 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/application.c +++ b/samples/cellular/nrf_cloud_multi_service/src/application.c @@ -28,6 +28,7 @@ #include "led_control.h" #include "at_commands.h" #include "shadow_config.h" +#include "gateway.h" LOG_MODULE_REGISTER(application, CONFIG_MULTI_SERVICE_LOG_LEVEL); @@ -364,6 +365,9 @@ void main_application_thread_fn(void) } dk_buttons_init(button_handler); +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + init_gateway(); +#endif /* Wait for first connection before starting the application. */ (void)await_cloud_ready(K_FOREVER); diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/ble.c b/samples/cellular/nrf_cloud_multi_service/src/ble/ble.c new file mode 100644 index 000000000000..508ecf6c37ae --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/ble.c @@ -0,0 +1,1598 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "net/nrf_cloud.h" +#include "ble.h" + +#include "ble_codec.h" +#include "gateway.h" +#include "ctype.h" +#include "nrf_cloud_transport.h" +#include "ble_conn_mgr.h" + +#define SEND_NOTIFY_STACK_SIZE 2048 +#define SEND_NOTIFY_PRIORITY 5 +#define SUBSCRIPTION_LIMIT 16 +#define NOTIFICATION_QUEUE_LIMIT 10 +#define MAX_BUF_SIZE 11000 +#define STR(x) #x +#define BT_UUID_GATT_CCC_VAL_STR STR(BT_UUID_GATT_CCC_VAL) + +#include +LOG_MODULE_REGISTER(ble, CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL); + +static char buffer[MAX_BUF_SIZE]; +static struct gw_msg output = { + .data.ptr = buffer, + .data.len = 0, + .data_max_len = MAX_BUF_SIZE +}; + +static bool discover_in_progress; +static bool scan_waiting; +static bool print_scan_results; + +static int num_devices_found; +static int num_names_found; + +struct k_timer rec_timer; +struct k_timer scan_timer; +struct k_timer auto_conn_start_timer; + +struct k_work scan_off_work; +struct k_work ble_device_encode_work; +struct k_work start_auto_conn_work; + +static atomic_t queued_notifications; + +struct ble_scanned_dev ble_scanned_devices[MAX_SCAN_RESULTS]; + +/* Must be statically allocated */ +/* TODO: The array needs to remain the entire time the sub exists. + * Should probably be stored with the conn manager. + */ +static struct bt_gatt_subscribe_params sub_param[BT_MAX_SUBSCRIBES]; +/* Array of connections corresponding to subscriptions above */ +static struct bt_conn *sub_conn[BT_MAX_SUBSCRIBES]; +static uint16_t sub_value[BT_MAX_SUBSCRIBES]; +static uint8_t curr_subs; +static int next_sub_index; + +static notification_cb_t notify_callback; + +struct rec_data_t { + void *fifo_reserved; + struct ble_device_conn *connected_ptr; + struct bt_gatt_subscribe_params sub_params; + struct bt_gatt_read_params read_params; + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + uint8_t data[256]; + bool read; + uint16_t length; +}; + +K_FIFO_DEFINE(rec_fifo); + +struct gatt_read_cache_entry { + uint16_t handle; + struct ble_device_conn *connected_ptr; + const struct bt_conn *conn; + struct uuid_handle_pair *uhp; + bool pending; + bool last; +} gatt_read_cache[CONFIG_BT_MAX_CONN]; + +/* Convert ble address string to uppcase */ +void bt_to_upper(char *ble_mac_addr_str, uint8_t addr_len) +{ + for (int i = 0; i < addr_len; i++) { + ble_mac_addr_str[i] = toupper(ble_mac_addr_str[i]); + } +} + +/* Get uuid string from bt_uuid object */ +void bt_uuid_get_str(const struct bt_uuid *uuid, char *str, size_t len) +{ + uint32_t tmp1, tmp5; + uint16_t tmp0, tmp2, tmp3, tmp4; + + switch (uuid->type) { + case BT_UUID_TYPE_16: + snprintk(str, len, "%04x", BT_UUID_16(uuid)->val); + break; + case BT_UUID_TYPE_32: + snprintk(str, len, "%08x", BT_UUID_32(uuid)->val); + break; + case BT_UUID_TYPE_128: + memcpy(&tmp0, &BT_UUID_128(uuid)->val[0], sizeof(tmp0)); + memcpy(&tmp1, &BT_UUID_128(uuid)->val[2], sizeof(tmp1)); + memcpy(&tmp2, &BT_UUID_128(uuid)->val[6], sizeof(tmp2)); + memcpy(&tmp3, &BT_UUID_128(uuid)->val[8], sizeof(tmp3)); + memcpy(&tmp4, &BT_UUID_128(uuid)->val[10], sizeof(tmp4)); + memcpy(&tmp5, &BT_UUID_128(uuid)->val[12], sizeof(tmp5)); + + snprintk(str, len, "%08x%04x%04x%04x%08x%04x", + tmp5, tmp4, tmp3, tmp2, tmp1, tmp0); + break; + default: + (void)memset(str, 0, len); + return; + } +} + +static int svc_attr_data_add(const struct bt_gatt_service_val *gatt_service, + uint16_t handle, struct ble_device_conn *ble_conn_ptr) +{ + char str[UUID_STR_LEN]; + + bt_uuid_get_str(gatt_service->uuid, str, sizeof(str)); + + bt_to_upper(str, strlen(str)); + LOG_INF("Service %s", str); + + return ble_conn_mgr_add_uuid_pair(gatt_service->uuid, handle, 0, 0, + BT_ATTR_SERVICE, ble_conn_ptr, true); +} + +static int chrc_attr_data_add(const struct bt_gatt_chrc *gatt_chrc, + struct ble_device_conn *ble_conn_ptr) +{ + uint16_t handle = gatt_chrc->value_handle; + + return ble_conn_mgr_add_uuid_pair(gatt_chrc->uuid, handle, 1, + gatt_chrc->properties, BT_ATTR_CHRC, + ble_conn_ptr, false); +} + +static int ccc_attr_data_add(const struct bt_uuid *uuid, uint16_t handle, + struct ble_device_conn *ble_conn_ptr) +{ + LOG_DBG("\tHandle: %d", handle); + + return ble_conn_mgr_add_uuid_pair(uuid, handle, 2, 0, BT_ATTR_CCC, + ble_conn_ptr, false); +} + +/* Add attributes to the connection manager objects */ +static int attr_add(const struct bt_gatt_dm *dm, + const struct bt_gatt_dm_attr *attr, + struct ble_device_conn *ble_conn_ptr) +{ + const struct bt_gatt_service_val *gatt_service = + bt_gatt_dm_attr_service_val(attr); + const struct bt_gatt_chrc *gatt_chrc = + bt_gatt_dm_attr_chrc_val(attr); + int err = 0; + + if ((bt_uuid_cmp(attr->uuid, BT_UUID_GATT_PRIMARY) == 0) || + (bt_uuid_cmp(attr->uuid, BT_UUID_GATT_SECONDARY) == 0)) { + err = svc_attr_data_add(gatt_service, attr->handle, + ble_conn_ptr); + } else if (bt_uuid_cmp(attr->uuid, BT_UUID_GATT_CHRC) == 0) { + err = chrc_attr_data_add(gatt_chrc, ble_conn_ptr); + } else if (bt_uuid_cmp(attr->uuid, BT_UUID_GATT_CCC) == 0) { + err = ccc_attr_data_add(attr->uuid, attr->handle, ble_conn_ptr); + } + return err; +} + +static void full_addr_to_mac_str(const char *raw_addr, char *ble_mac_addr_str) +{ + memcpy(ble_mac_addr_str, raw_addr, BT_ADDR_LE_DEVICE_LEN); + ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN] = 0; + bt_to_upper(ble_mac_addr_str, BT_ADDR_LE_DEVICE_LEN); +} + +static void le_addr_to_mac_addr_str(const bt_addr_le_t *le_addr, char *ble_mac_addr_str) +{ + char raw_addr[BT_ADDR_LE_STR_LEN + 1]; + + bt_addr_le_to_str(le_addr, raw_addr, BT_ADDR_LE_STR_LEN); + full_addr_to_mac_str(raw_addr, ble_mac_addr_str); +} + +static void conn_to_mac_addr_str(const struct bt_conn *conn, char *ble_mac_addr_str) +{ + return le_addr_to_mac_addr_str(bt_conn_get_dst(conn), ble_mac_addr_str); +} + +static int ble_dm_data_add(struct bt_gatt_dm *dm) +{ + const struct bt_gatt_dm_attr *attr = NULL; + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + struct ble_device_conn *ble_conn_ptr; + struct bt_conn *conn; + int err; + + conn = bt_gatt_dm_conn_get(dm); + conn_to_mac_addr_str(conn, ble_mac_addr_str); + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &ble_conn_ptr); + if (err) { + LOG_ERR("Connection not found for addr %s", ble_mac_addr_str); + return err; + } + + discover_in_progress = true; + + attr = bt_gatt_dm_service_get(dm); + + err = attr_add(dm, attr, ble_conn_ptr); + if (err) { + LOG_ERR("Unable to add attribute: %d", err); + return err; + } + + while (NULL != (attr = bt_gatt_dm_attr_next(dm, attr))) { + err = attr_add(dm, attr, ble_conn_ptr); + if (err) { + LOG_ERR("Unable to add attribute: %d", err); + return err; + } + } + return 0; +} + +void ble_register_notify_callback(notification_cb_t callback) +{ + notify_callback = callback; +} + +/* Thread responsible for transferring ble data over MQTT */ +static void send_ble_read_data(int unused1, int unused2, int unused3) +{ + ARG_UNUSED(unused1); + ARG_UNUSED(unused2); + ARG_UNUSED(unused3); + char uuid[BT_UUID_STR_LEN]; + char path[BT_MAX_PATH_LEN]; + + memset(uuid, 0, BT_UUID_STR_LEN); + memset(path, 0, BT_MAX_PATH_LEN); + + struct ble_device_conn *connected_ptr; + + while (1) { + int err; + uint16_t handle; + struct rec_data_t *rx_data = k_fifo_get(&rec_fifo, K_FOREVER); + + if (rx_data == NULL) { + continue; + } + atomic_dec(&queued_notifications); + LOG_DBG("Queued: %ld", atomic_get(&queued_notifications)); + + connected_ptr = rx_data->connected_ptr; + if (connected_ptr == NULL) { + LOG_ERR("Bad connected_ptr!"); + goto cleanup; + } + + if (rx_data->read) { + handle = rx_data->read_params.single.handle; + LOG_INF("Dequeued gatt_read results, addr %s handle %d", + rx_data->ble_mac_addr_str, handle); + } else { + handle = rx_data->sub_params.value_handle; + LOG_DBG("Dequeued BLE notification data, addr %s handle %d", + rx_data->ble_mac_addr_str, handle); + } + /* LOG_HEXDUMP_DBG(rx_data->data, rx_data->length, "notify"); */ + + err = ble_conn_mgr_get_uuid_by_handle(handle, uuid, connected_ptr); + if (err) { + if (discover_in_progress) { + LOG_INF("Ignoring notification on %s due to BLE" + " discovery in progress", + rx_data->ble_mac_addr_str); + } else { + LOG_ERR("Unable to convert handle: %d", err); + } + goto cleanup; + } + + bool ccc = !rx_data->read; + + if (strcmp(uuid, BT_UUID_GATT_CCC_VAL_STR) == 0) { + ccc = true; + handle--; + LOG_INF("Force ccc for handle %u", handle); + } + + err = ble_conn_mgr_generate_path(connected_ptr, handle, path, ccc); + if (err) { + LOG_ERR("Unable to generate path: %d", err); + goto cleanup; + } + + if (!rx_data->read && notify_callback) { + err = notify_callback(rx_data->ble_mac_addr_str, uuid, + rx_data->data, rx_data->length); + if (err) { + /* callback should return 0 if it did not + * process the data + */ + goto cleanup; + } + } + if (connected_ptr && connected_ptr->hidden) { + LOG_DBG("Suppressing notification write to cloud"); + goto cleanup; + } + + k_mutex_lock(&output.lock, K_FOREVER); + if (rx_data->read && !ccc) { + err = device_chrc_read_encode(rx_data->ble_mac_addr_str, + uuid, path, ((char *)rx_data->data), + rx_data->length, &output); + } else if (rx_data->read && ccc) { + err = device_descriptor_value_encode(rx_data->ble_mac_addr_str, + BT_UUID_GATT_CCC_VAL_STR, + path, + ((char *)rx_data->data), + rx_data->length, + &output, false); + } else { + err = device_value_changed_encode(rx_data->ble_mac_addr_str, + uuid, path, ((char *)rx_data->data), + rx_data->length, &output); + } + if (err) { + k_mutex_unlock(&output.lock); + LOG_ERR("Unable to encode: %d", err); + goto cleanup; + } + err = g2c_send(&output.data); + k_mutex_unlock(&output.lock); + if (err) { + LOG_ERR("Unable to send: %d", err); + } + +cleanup: + k_free(rx_data); + } +} + +K_THREAD_DEFINE(ble_rec_thread, SEND_NOTIFY_STACK_SIZE, + send_ble_read_data, NULL, NULL, NULL, + SEND_NOTIFY_PRIORITY, 0, 0); + +static void discovery_completed(struct bt_gatt_dm *disc, void *ctx) +{ + LOG_DBG("Attribute count: %d", bt_gatt_dm_attr_cnt(disc)); + + int err; + + err = ble_dm_data_add(disc); + /* TBD: how can we cancel remainder of discovery if we get an error? */ + + bt_gatt_dm_data_release(disc); + + bt_gatt_dm_continue(disc, NULL); +} + +static void discovery_service_complete(struct bt_conn *conn, void *ctx) +{ + LOG_DBG("Discovery complete."); + + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + struct ble_device_conn *connected_ptr; + int err; + + conn_to_mac_addr_str(conn, ble_mac_addr_str); + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + LOG_ERR("Connection not found for addr %s", ble_mac_addr_str); + } else { + /* only set discovered true and send results if it seems we were + * successful at doing a full discovery + */ + if (connected_ptr->connected && connected_ptr->num_pairs) { + connected_ptr->encode_discovered = true; + connected_ptr->discovered = true; + } else { + LOG_WRN("Discovery not completed"); + } + connected_ptr->discovering = false; + } + discover_in_progress = false; + + /* check scan waiting */ + if (scan_waiting) { + scan_start(print_scan_results); + } +} + +static void discovery_error_found(struct bt_conn *conn, int err, void *ctx) +{ + LOG_ERR("The discovery procedure failed, err %d", err); + + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + struct ble_device_conn *connected_ptr; + + conn_to_mac_addr_str(conn, ble_mac_addr_str); + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + LOG_ERR("Connection not found for addr %s", ble_mac_addr_str); + } else { + connected_ptr->num_pairs = 0; + connected_ptr->discovering = false; + connected_ptr->discovered = false; + } + discover_in_progress = false; + + if (IS_ENABLED(CONFIG_SETTINGS)) { + LOG_INF("Saving settings"); + settings_save(); + } + + /* Disconnect? */ + bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + + /* check scan waiting */ + if (scan_waiting) { + scan_start(print_scan_results); + } +} + +static int find_gatt_read_cache_entry(const struct bt_conn *conn, uint16_t handle) +{ + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if ((gatt_read_cache[i].conn == conn) && + (gatt_read_cache[i].handle == handle)) { + return i; + } + } + return -1; +} + +static bool store_gatt_read_cache(struct ble_device_conn *connected_ptr, + const struct bt_conn *conn, uint16_t handle, + struct uuid_handle_pair *uhp) +{ + int entry = find_gatt_read_cache_entry(conn, handle); + + if (entry == -1) { + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (!gatt_read_cache[i].pending) { + entry = i; + break; + } + } + if (entry == -1) { + LOG_ERR("OUT OF GATT READ CACHE ENTRIES!"); + return false; + } + } + struct gatt_read_cache_entry *c = &gatt_read_cache[entry]; + + c->pending = true; + c->connected_ptr = connected_ptr; + c->conn = conn; + c->handle = handle; + c->uhp = uhp; + c->last = false; + + return true; +} + +static uint8_t gatt_read_callback(struct bt_conn *conn, uint8_t err, + struct bt_gatt_read_params *params, + const void *data, uint16_t length) +{ + int ret = BT_GATT_ITER_CONTINUE; + int entry = find_gatt_read_cache_entry(conn, params->single.handle); + + if (entry == -1) { + return BT_GATT_ITER_STOP; + } + struct gatt_read_cache_entry *c = &gatt_read_cache[entry]; + struct uuid_handle_pair *uhp = c->uhp; + + if ((length > 0) && (data != NULL)) { + /* Store value so next discovery of this device sends the + * most recent value for each readable characteristic, + * rather than 0. + */ + uint16_t offset = MIN(params->single.offset, BT_MAX_VALUE_LEN); + uint16_t this_len = MIN(length, (BT_MAX_VALUE_LEN - offset)); + + uhp->value_len = offset + this_len; + memcpy(&uhp->value[offset], data, this_len); + } + + /* End of read */ + if ((data == NULL) || (length < 22)) { + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + size_t size = sizeof(struct rec_data_t); + struct rec_data_t *read_data = (struct rec_data_t *)k_malloc(size); + + if (read_data == NULL) { + LOG_ERR("Out of memory error in gatt_read_callback(): " + "%ld queued notifications", + atomic_get(&queued_notifications)); + ret = BT_GATT_ITER_STOP; + return ret; + } + + conn_to_mac_addr_str(conn, ble_mac_addr_str); + + memset(&read_data->sub_params, 0, sizeof(read_data->sub_params)); + memcpy(&read_data->read_params, params, sizeof(struct bt_gatt_read_params)); + memcpy(&read_data->ble_mac_addr_str, ble_mac_addr_str, BT_ADDR_LE_DEVICE_LEN + 1); + memcpy(&read_data->data, uhp->value, uhp->value_len); + read_data->fifo_reserved = NULL; + read_data->read = true; + read_data->length = uhp->value_len; + read_data->connected_ptr = c->connected_ptr; + + atomic_inc(&queued_notifications); + k_fifo_put(&rec_fifo, read_data); + LOG_DBG("Enqueued gatt_read of %d bytes; queued:%ld", + read_data->length, atomic_get(&queued_notifications)); + c->pending = false; + + ret = BT_GATT_ITER_STOP; + } + + return ret; +} + +static int gatt_read_handle(struct ble_device_conn *connected_ptr, + struct uuid_handle_pair *uhp, bool ccc) +{ + int err; + static struct bt_gatt_read_params params; + struct bt_conn *conn; + uint16_t handle = uhp->handle; + + if (ccc) { + handle++; + } + params.handle_count = 1; + params.single.handle = handle; + params.single.offset = 0; + params.func = gatt_read_callback; + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &connected_ptr->bt_addr); + if (conn == NULL) { + LOG_ERR("Null Conn object"); + return -EINVAL; + } + + (void)store_gatt_read_cache(connected_ptr, conn, handle, uhp); + err = bt_gatt_read(conn, ¶ms); + bt_conn_unref(conn); + return err; +} + +int gatt_read(const char *ble_mac_addr_str, const char *chrc_uuid, bool ccc) +{ + int err; + struct ble_device_conn *connected_ptr; + struct uuid_handle_pair *uhp; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + return err; + } + + uhp = ble_conn_mgr_get_uhp_by_uuid(chrc_uuid, connected_ptr); + if (!uhp) { + return 1; + } + + LOG_DBG("Read %s, 0x%02X, %d", chrc_uuid, + ccc ? uhp->handle : uhp->handle + 1, ccc); + return gatt_read_handle(connected_ptr, uhp, ccc); +} + +static void on_sent(struct bt_conn *conn, uint8_t err, + struct bt_gatt_write_params *params) +{ + ARG_UNUSED(conn); + + LOG_DBG("Sent Data of Length: %d", params->length); +} + +int gatt_write(const char *ble_mac_addr_str, const char *chrc_uuid, uint8_t *data, + uint16_t data_len, bt_gatt_write_func_t cb) +{ + int err; + struct bt_conn *conn; + struct ble_device_conn *connected_ptr; + uint16_t handle; + static struct bt_gatt_write_params params; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + return err; + } + + err = ble_conn_mgr_get_handle_by_uuid(&handle, chrc_uuid, + connected_ptr); + if (err) { + return err; + } + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &connected_ptr->bt_addr); + if (conn == NULL) { + LOG_ERR("Null Conn object"); + return -EINVAL; + } + + LOG_DBG("Writing to addr: %s to chrc %s with handle %d", + ble_mac_addr_str, chrc_uuid, handle); + LOG_HEXDUMP_DBG(data, data_len, "Data to write"); + + params.func = cb ? cb : on_sent; + params.handle = handle; + params.offset = 0; + params.data = data; + params.length = data_len; + + err = bt_gatt_write(conn, ¶ms); + bt_conn_unref(conn); + return err; +} + +int gatt_write_without_response(const char *ble_mac_addr_str, const char *chrc_uuid, + uint8_t *data, uint16_t data_len) +{ + int err; + + struct bt_conn *conn; + struct ble_device_conn *connected_ptr; + uint16_t handle; + /* static struct bt_gatt_write_params params; */ + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + return err; + } + + err = ble_conn_mgr_get_handle_by_uuid(&handle, chrc_uuid, + connected_ptr); + if (err) { + return err; + } + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &connected_ptr->bt_addr); + if (conn == NULL) { + LOG_ERR("Null Conn object"); + return -EINVAL; + } + + LOG_DBG("Writing w/o resp to addr: %s to chrc %s with handle %d", + ble_mac_addr_str, chrc_uuid, handle); + LOG_HEXDUMP_DBG(data, data_len, "Data to write"); + + err = bt_gatt_write_without_response(conn, handle, data, data_len, + false); + bt_conn_unref(conn); + return err; +} + +static uint8_t on_notification_received(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + int ret = BT_GATT_ITER_CONTINUE; + + if (!data) { + return BT_GATT_ITER_STOP; + } + + if ((length > 0) && (data != NULL)) { + conn_to_mac_addr_str(conn, ble_mac_addr_str); + struct ble_device_conn *connected_ptr; + int err; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + return err; + } + + struct rec_data_t tx_data = { + .connected_ptr = connected_ptr, + .read = false, + .length = length + }; + + memcpy(&tx_data.ble_mac_addr_str, ble_mac_addr_str, BT_ADDR_LE_DEVICE_LEN + 1); + memcpy(&tx_data.data, data, length); + memcpy(&tx_data.sub_params, params, + sizeof(struct bt_gatt_subscribe_params)); + + size_t size = sizeof(struct rec_data_t); + + if (atomic_get(&queued_notifications) >= + NOTIFICATION_QUEUE_LIMIT) { + struct rec_data_t *rx_data = k_fifo_get(&rec_fifo, K_NO_WAIT); + + LOG_INF("Dropping oldest message"); + if (rx_data != NULL) { + uint16_t h; + + if (rx_data->read) { + h = rx_data->read_params.single.handle; + } else { + h = rx_data->sub_params.value_handle; + } + LOG_INF("Addr %s Handle %d Queued %ld", + rx_data->ble_mac_addr_str, + h, + atomic_get(&queued_notifications)); + k_free(rx_data); + } + atomic_dec(&queued_notifications); + } + + char *mem_ptr = k_malloc(size); + + if (mem_ptr == NULL) { + LOG_ERR("Out of memory error in on_received(): " + "%ld queued notifications", + atomic_get(&queued_notifications)); + ret = BT_GATT_ITER_STOP; + } else { + atomic_inc(&queued_notifications); + memcpy(mem_ptr, &tx_data, size); + k_fifo_put(&rec_fifo, mem_ptr); + LOG_DBG("Enqueued BLE notification data %ld", + atomic_get(&queued_notifications)); + } + } + + return ret; +} + +static void send_sub(const char *ble_mac_addr_str, const char *path, + struct gw_msg *out, uint8_t *value) +{ + k_mutex_lock(&out->lock, K_FOREVER); + device_descriptor_value_encode(ble_mac_addr_str, + BT_UUID_GATT_CCC_VAL_STR, + path, value, sizeof(value), + out, true); + g2c_send(&out->data); + device_value_write_result_encode(ble_mac_addr_str, + BT_UUID_GATT_CCC_VAL_STR, + path, value, sizeof(value), + out); + g2c_send(&out->data); + k_mutex_unlock(&out->lock); +} + +int ble_subscribe(const char *ble_mac_addr_str, const char *chrc_uuid, uint8_t value_type) +{ + int err; + char path[BT_MAX_PATH_LEN]; + struct bt_conn *conn = NULL; + struct ble_device_conn *connected_ptr; + uint16_t handle; + bool subscribed; + uint8_t param_index; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + LOG_ERR("Connection not found for addr %s", ble_mac_addr_str); + return -ENOLINK; + } + + ble_conn_mgr_get_handle_by_uuid(&handle, chrc_uuid, connected_ptr); + + ble_conn_mgr_get_subscribed(handle, connected_ptr, &subscribed, + ¶m_index); + + if (connected_ptr->connected) { + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &connected_ptr->bt_addr); + if (conn == NULL) { + /* work around strange error on Flic button -- + * type changes when it should not + */ + connected_ptr->bt_addr.type = 0; + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &connected_ptr->bt_addr); + if (conn == NULL) { + LOG_ERR("Null Conn object"); + err = -EINVAL; + goto end; + } + } + } + + err = ble_conn_mgr_generate_path(connected_ptr, handle, path, true); + if (err) { + return err; + } + + if (subscribed && (value_type == 0)) { + /* If subscribed then unsubscribe. */ + if (conn != NULL) { + bt_gatt_unsubscribe(conn, &sub_param[param_index]); + } + ble_conn_mgr_remove_subscribed(handle, connected_ptr); + + uint8_t value[2] = {0, 0}; + + if (connected_ptr && connected_ptr->hidden) { + LOG_DBG("suppressing value_changed"); + } else { + send_sub(ble_mac_addr_str, path, &output, value); + } + LOG_INF("Unsubscribe: Addr %s Handle %d", + ble_mac_addr_str, handle); + + sub_value[param_index] = 0; + sub_conn[param_index] = NULL; + if (curr_subs) { + curr_subs--; + } + } else if (subscribed && (value_type != 0)) { + uint8_t value[2] = { + value_type, + 0 + }; + + if (connected_ptr && connected_ptr->hidden) { + LOG_DBG("suppressing value_changed"); + } else { + send_sub(ble_mac_addr_str, path, &output, value); + } + LOG_INF("Subscribe Dup: Addr %s Handle %d %s", + ble_mac_addr_str, handle, + (value_type == BT_GATT_CCC_NOTIFY) ? + "Notify" : "Indicate"); + + } else if (value_type == 0) { + LOG_DBG("Unsubscribe N/A: Addr %s Handle %d", + ble_mac_addr_str, handle); + } else if (curr_subs < SUBSCRIPTION_LIMIT) { + if (conn != NULL) { + sub_param[next_sub_index].notify = on_notification_received; + sub_param[next_sub_index].value = value_type; + sub_param[next_sub_index].value_handle = handle; + sub_param[next_sub_index].ccc_handle = handle + 1; + sub_value[next_sub_index] = value_type; + sub_conn[next_sub_index] = conn; + err = bt_gatt_subscribe(conn, &sub_param[next_sub_index]); + if (err) { + LOG_ERR("Subscribe failed (err %d)", err); + goto end; + } + } + ble_conn_mgr_set_subscribed(handle, next_sub_index, + connected_ptr); + + uint8_t value[2] = { + value_type, + 0 + }; + + if (connected_ptr && connected_ptr->hidden) { + LOG_DBG("suppressing value_changed"); + } else { + send_sub(ble_mac_addr_str, path, &output, value); + } + LOG_INF("Subscribe: Addr %s Handle %d %s", + ble_mac_addr_str, handle, + (value_type == BT_GATT_CCC_NOTIFY) ? + "Notify" : "Indicate"); + + curr_subs++; + next_sub_index++; + } else { + char msg[64]; + + sprintf(msg, "Reached subscription limit of %d", + SUBSCRIPTION_LIMIT); + + /* Send error when limit is reached. */ + k_mutex_lock(&output.lock, K_FOREVER); + device_error_encode(ble_mac_addr_str, msg, &output); + g2c_send(&output.data); + k_mutex_unlock(&output.lock); + } + +end: + if (conn != NULL) { + bt_conn_unref(conn); + } + return err; +} + +int ble_subscribe_handle(const char *ble_mac_addr_str, uint16_t handle, uint8_t value_type) +{ + char uuid[BT_UUID_STR_LEN]; + struct ble_device_conn *conn_ptr; + int err; + + LOG_DBG("Addr %s handle %u value_type %u", ble_mac_addr_str, + handle, (unsigned int)value_type); + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &conn_ptr); + if (err == 0) { + err = ble_conn_mgr_get_uuid_by_handle(handle, uuid, conn_ptr); + if (err == 0) { + LOG_DBG("Subscribing uuid %s", + uuid); + err = ble_subscribe(ble_mac_addr_str, uuid, value_type); + } + } + return err; +} + +int ble_subscribe_all(const char *ble_mac_addr_str, uint8_t value_type) +{ + unsigned int i; + struct ble_device_conn *conn_ptr; + struct uuid_handle_pair *up; + int err; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &conn_ptr); + if (err) { + return err; + } + + for (i = 0; i < conn_ptr->num_pairs; i++) { + up = conn_ptr->uuid_handle_pairs[i]; + if (up == NULL) { + continue; + } + if ((up->properties & BT_GATT_CHRC_NOTIFY) != + BT_GATT_CHRC_NOTIFY) { + continue; + } + err = ble_subscribe_handle(ble_mac_addr_str, up->handle, value_type); + if (err) { + break; + } + } + return err; +} + + +static int ble_subscribe_device(struct bt_conn *conn, bool subscribe) +{ + uint16_t handle; + int i; + int count = 0; + struct ble_device_conn *connected_ptr; + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + int err; + + if (conn == NULL) { + return -EINVAL; + } + conn_to_mac_addr_str(conn, ble_mac_addr_str); + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ptr); + if (err) { + LOG_ERR("Connection not found for addr %s", ble_mac_addr_str); + return -EINVAL; + } + + for (i = 0; i < BT_MAX_SUBSCRIBES; i++) { + if (conn == sub_conn[i]) { + handle = sub_param[i].value_handle; + if (subscribe && sub_value[i]) { + sub_param[i].value = sub_value[i]; + bt_gatt_subscribe(conn, &sub_param[i]); + ble_conn_mgr_set_subscribed(handle, i, + connected_ptr); + LOG_INF("Subscribe: Addr %s Handle %d Idx %d", + ble_mac_addr_str, handle, i); + count++; + } else { + bt_gatt_unsubscribe(conn, &sub_param[i]); + ble_conn_mgr_remove_subscribed(handle, + connected_ptr); + sub_conn[i] = NULL; + if (curr_subs) { + curr_subs--; + } + LOG_INF("Unsubscribe: Addr %s Handle %d Idx %d", + ble_mac_addr_str, handle, i); + count++; + } + } + } + LOG_INF("Subscriptions changed for %d handles", count); + return 0; +} + +static struct bt_gatt_dm_cb discovery_cb = { + .completed = discovery_completed, + .service_not_found = discovery_service_complete, + .error_found = discovery_error_found, +}; + +int ble_discover(struct ble_device_conn *connection_ptr) +{ + int err = 0; + char *ble_mac_addr_str = connection_ptr->ble_mac_addr_str; + struct bt_conn *conn = NULL; + + LOG_INF("Discovering: %s\n", ble_mac_addr_str); + + if (!discover_in_progress) { + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &connection_ptr->bt_addr); + if (conn == NULL) { + LOG_DBG("ERROR: Null Conn object"); + return -EINVAL; + } + + if (!connection_ptr->discovered) { + if (connection_ptr->num_pairs) { + LOG_INF("Marking device as discovered; " + "num pairs = %u", + connection_ptr->num_pairs); + connection_ptr->discovering = false; + connection_ptr->discovered = true; + connection_ptr->encode_discovered = true; + bt_conn_unref(conn); + return 0; + } + discover_in_progress = true; + connection_ptr->discovering = true; + + for (int i = 1; i < 3; i++) { + if (i > 1) { + LOG_INF("Retrying..."); + } + err = bt_gatt_dm_start(conn, NULL, &discovery_cb, NULL); + if (!err) { + break; + } + LOG_WRN("Service discovery err %d", err); + k_sleep(K_MSEC(500)); + } + if (err) { + LOG_ERR("Aborting discovery. Disconnecting from device..."); + connection_ptr->discovering = false; + discover_in_progress = false; + bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + bt_conn_unref(conn); + ble_conn_set_connected(connection_ptr, false); + return err; + } + } else { + connection_ptr->encode_discovered = true; + } + } else { + return -EBUSY; + } + + bt_conn_unref(conn); + return err; +} + +int disconnect_device_by_addr(const char *ble_mac_addr_str) +{ + int err; + struct bt_conn *conn; + bt_addr_le_t bt_addr; + + err = bt_addr_le_from_str(ble_mac_addr_str, "random", &bt_addr); + if (err) { + LOG_ERR("Address from string failed (err %d)", err); + return err; + } + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &bt_addr); + if (conn == NULL) { + LOG_DBG("ERROR: Null Conn object"); + return -EINVAL; + } + + /* cloud commanded this, so remove any notifications or indications */ + err = ble_subscribe_device(conn, false); + if (err) { + LOG_ERR("Error unsubscribing device: %d", err); + } + + /* Disconnect device. */ + err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err) { + LOG_ERR("Error disconnecting: %d", err); + } + bt_conn_unref(conn); + + return err; +} + +int set_shadow_ble_conn(const char *ble_mac_addr_str, bool connecting, bool connected) +{ + int err; + + LOG_INF("Connecting=%u, connected=%u", connecting, connected); + k_mutex_lock(&output.lock, K_FOREVER); + err = device_shadow_data_encode(ble_mac_addr_str, connecting, connected, + &output); + if (!err) { + err = gw_shadow_publish(&output.data); + if (err) { + LOG_ERR("nrf_cloud_gw_shadow_publish() failed %d", err); + } + } else { + LOG_ERR("device_shadow_data_encode() failed %d", err); + } + k_mutex_unlock(&output.lock); + return err; +} + +int set_shadow_desired_conn(const struct desired_conn *desired, int num_desired) +{ + int err; + + k_mutex_lock(&output.lock, K_FOREVER); + err = gateway_desired_list_encode(desired, + num_desired, + &output); + if (!err) { + err = gw_shadow_publish(&output.data); + if (err) { + LOG_ERR("gw_shadow_publish() failed %d", err); + } + } else { + LOG_ERR("Failed %d", err); + } + k_mutex_unlock(&output.lock); + + return err; +} + +static void auto_conn_start_work_handler(struct k_work *work) +{ + int err; + + /* Restart to scanning for allowlisted devices */ + struct bt_conn_le_create_param param = BT_CONN_LE_CREATE_PARAM_INIT( + BT_CONN_LE_OPT_NONE, + BT_GAP_SCAN_FAST_INTERVAL, + BT_GAP_SCAN_FAST_WINDOW); + + err = bt_conn_le_create_auto(¶m, BT_LE_CONN_PARAM_DEFAULT); + + if (err == -EALREADY) { + LOG_INF("Auto connect already running"); + } else if (err == -EINVAL) { + LOG_INF("Auto connect not necessary or invalid"); + } else if (err == -EAGAIN) { + LOG_WRN("Device not ready; try starting auto connect later"); + k_timer_start(&auto_conn_start_timer, K_SECONDS(3), K_SECONDS(0)); + } else if (err == -ENOMEM) { + LOG_ERR("Out of memory enabling auto connect"); + } else if (err) { + LOG_ERR("Error enabling auto connect: %d", err); + } else { + LOG_INF("Auto connect started"); + } +} + +K_WORK_DEFINE(auto_conn_start_work, auto_conn_start_work_handler); + +static void auto_conn_start_timer_handler(struct k_timer *timer) +{ + k_work_submit(&auto_conn_start_work); +} + +K_TIMER_DEFINE(auto_conn_start_timer, auto_conn_start_timer_handler, NULL); + +static void connected(struct bt_conn *conn, uint8_t conn_err) +{ + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + struct ble_device_conn *connection_ptr; + int err; + + conn_to_mac_addr_str(conn, ble_mac_addr_str); + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connection_ptr); + if (err) { + LOG_ERR("Connection not found for addr %s", ble_mac_addr_str); + } + if (conn_err || err) { + LOG_ERR("Failed to connect to %s (%u)", ble_mac_addr_str, + conn_err); + if (connection_ptr) { + ble_conn_set_connected(connection_ptr, false); + } + return; + } + + if (connection_ptr && connection_ptr->hidden) { + LOG_DBG("suppressing device_connect"); + } else { + k_mutex_lock(&output.lock, K_FOREVER); + device_connect_result_encode(ble_mac_addr_str, true, &output); + g2c_send(&output.data); + k_mutex_unlock(&output.lock); + } + + if (connection_ptr->added_to_allowlist) { + if (!ble_add_to_allowlist(ble_mac_addr_str, false)) { + connection_ptr->added_to_allowlist = false; + } + } + + /* TODO: set LED pattern indicating BLE device is connected */ + if (!connection_ptr->connected) { + LOG_INF("Connected: %s", ble_mac_addr_str); + ble_conn_set_connected(connection_ptr, true); + ble_subscribe_device(conn, true); + if (!connection_ptr->hidden) { + set_shadow_ble_conn(ble_mac_addr_str, false, true); + } + } else { + LOG_INF("Reconnected: %s", ble_mac_addr_str); + } + + /* Start the timer to begin scanning again. */ + k_timer_start(&auto_conn_start_timer, K_SECONDS(3), K_SECONDS(0)); +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + struct ble_device_conn *connection_ptr = NULL; + + conn_to_mac_addr_str(conn, ble_mac_addr_str); + ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connection_ptr); + + if (!ble_conn_mgr_is_desired(ble_mac_addr_str)) { + LOG_INF("suppressing device_disconnect"); + } else { + k_mutex_lock(&output.lock, K_FOREVER); + device_disconnect_result_encode(ble_mac_addr_str, false, &output); + g2c_send(&output.data); + k_mutex_unlock(&output.lock); + } + + /* if device disconnected on purpose, don't bother updating + * shadow; it will likely reconnect shortly + */ + if (reason != BT_HCI_ERR_REMOTE_USER_TERM_CONN) { + LOG_INF("Disconnected: %s (reason 0x%02x)", ble_mac_addr_str, + reason); + if (connection_ptr && !connection_ptr->hidden) { + (void)set_shadow_ble_conn(ble_mac_addr_str, false, false); + } + if (connection_ptr) { + ble_conn_set_connected(connection_ptr, false); + } + } else { + LOG_INF("Disconnected: temporary"); + } + + if (connection_ptr && + !connection_ptr->free && + !connection_ptr->added_to_allowlist) { + if (!ble_add_to_allowlist(connection_ptr->ble_mac_addr_str, true)) { + connection_ptr->added_to_allowlist = true; + } + } + + /* TODO: set BLE device disconnected LED pattern */ + + /* Start the timer to begin scanning again. */ + k_timer_start(&auto_conn_start_timer, K_SECONDS(3), K_SECONDS(0)); +} + +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +static bool data_cb(struct bt_data *data, void *user_data) +{ + char *name = user_data; + int len = MIN(data->data_len, NAME_LEN - 1); + + switch (data->type) { + case BT_DATA_NAME_SHORTENED: + case BT_DATA_NAME_COMPLETE: + memcpy(name, data->data, len); + name[len] = '\0'; + return false; + default: + return true; + } +} + +static void ble_device_found_enc_handler(struct k_work *work) +{ + LOG_DBG("Encoding scan..."); + k_mutex_lock(&output.lock, K_FOREVER); + device_found_encode(num_devices_found, &output); + LOG_DBG("Sending scan..."); + g2c_send(&output.data); + k_mutex_unlock(&output.lock); +} + +K_WORK_DEFINE(ble_device_encode_work, ble_device_found_enc_handler); + +static void device_found(const bt_addr_le_t *bt_addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + char name[NAME_LEN]; + struct ble_scanned_dev *scanned = &ble_scanned_devices[num_devices_found]; + char ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN + 1]; + + if (num_devices_found >= MAX_SCAN_RESULTS) { + return; + } + + /* We're only interested in connectable events */ + if (type != BT_HCI_ADV_IND && type != BT_HCI_ADV_DIRECT_IND) { + return; + } + + le_addr_to_mac_addr_str(bt_addr, ble_mac_addr_str); + + /* Check for duplicate addresses before adding to results. */ + for (int j = 0; j < num_devices_found; j++) { + if (!(strncasecmp(ble_mac_addr_str, + ble_scanned_devices[j].ble_mac_addr_str, + BT_ADDR_LE_STR_LEN))) { + return; /* no need to continue; we saw this */ + } + } + /* Update scan results to include this new device's MAC address. */ + memcpy(scanned->ble_mac_addr_str, ble_mac_addr_str, BT_ADDR_LE_DEVICE_LEN); + scanned->ble_mac_addr_str[BT_ADDR_LE_DEVICE_LEN] = '\0'; + + /* Add the device name, if known */ + memset(name, 0, sizeof(name)); + bt_data_parse(ad, data_cb, name); + strcpy(scanned->name, name); + if (strlen(name)) { + num_names_found++; + } + + scanned->rssi = (int)rssi; + num_devices_found++; + + LOG_INF("%d. %s, %d, %s", num_devices_found, scanned->ble_mac_addr_str, + rssi, scanned->name); +} + +struct ble_scanned_dev *get_scanned_device(unsigned int i) +{ + if (i < num_devices_found) { + return &ble_scanned_devices[i]; + } else { + return NULL; + } +} + +int get_num_scan_results(void) +{ + return num_devices_found; +} + +int get_num_scan_names(void) +{ + return num_names_found; +} + +static void scan_off_handler(struct k_work *work) +{ + int err; + + LOG_INF("Stopping scan..."); + + err = bt_le_scan_stop(); + if (err) { + LOG_INF("Stopping scanning failed (err %d)", err); + } else { + LOG_INF("Scan successfully stopped"); + } + + /* Start the timer to begin scanning again. */ + k_timer_start(&auto_conn_start_timer, K_SECONDS(3), K_SECONDS(0)); + + LOG_DBG("Submitting scan..."); + k_work_submit(&ble_device_encode_work); + + if (print_scan_results) { + print_scan_results = false; + + struct ble_scanned_dev *scanned; + int i; + + printk("Scan results:\n"); + for (i = 0, scanned = ble_scanned_devices; + i < num_devices_found; i++, scanned++) { + printk("%d. %s, %d, %s\n", i, scanned->ble_mac_addr_str, + (int)scanned->rssi, scanned->name); + k_sleep(K_MSEC(50)); + } + } +} + +K_WORK_DEFINE(scan_off_work, scan_off_handler); + +static void scan_timer_handler(struct k_timer *timer) +{ + k_work_submit(&scan_off_work); +} + +K_TIMER_DEFINE(scan_timer, scan_timer_handler, NULL); + +int ble_add_to_allowlist(const char *addr_str, bool add) +{ + int err; + bt_addr_le_t bt_addr; + + LOG_INF("%s allowlist: %s", add ? "Adding address to" : "Removing address from", + addr_str); + + bt_conn_create_auto_stop(); + + err = bt_addr_le_from_str(addr_str, "random", &bt_addr); + if (err) { + LOG_ERR("Invalid peer address: %d", err); + goto done; + } + + err = add ? bt_le_filter_accept_list_add(&bt_addr) : + bt_le_filter_accept_list_remove(&bt_addr); + if (err) { + LOG_ERR("Error %s allowlist: %d", add ? "adding to" : "removing from", err); + } + +done: + /* Start the timer to begin scanning again. */ + k_timer_start(&auto_conn_start_timer, K_SECONDS(3), K_SECONDS(0)); + + return err; +} + +void ble_stop_activity(void) +{ + int err; + + LOG_INF("Stop scan..."); + err = bt_le_scan_stop(); + if (err) { + LOG_DBG("Error stopping scan: %d", err); + } + + LOG_INF("Stop autoconnect..."); + err = bt_conn_create_auto_stop(); + if (err) { + LOG_DBG("Error stopping autoconnect: %d", err); + } + + LOG_INF("Clear allowlist..."); + err = bt_le_filter_accept_list_clear(); + if (err) { + LOG_DBG("Error clearing allowlist: %d", err); + } + + struct ble_device_conn *conn; + uint8_t ret; + + LOG_INF("Disconnect devices..."); + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + conn = get_connected_device(i); + if (conn && (conn->connected)) { + ret = disconnect_device_by_addr(conn->ble_mac_addr_str); + if (ret) { + LOG_DBG("Error disconnecting %s", + conn->ble_mac_addr_str); + } + } + } +} + +int device_discovery_send(struct ble_device_conn *conn_ptr) +{ + if (conn_ptr && conn_ptr->hidden) { + LOG_DBG("suppressing device_discovery_send"); + return 0; + } + k_mutex_lock(&output.lock, K_FOREVER); + int ret = device_discovery_encode(conn_ptr, &output); + + if (!ret) { + LOG_INF("Sending discovery; JSON Size: %d", output.data.len); + g2c_send(&output.data); + } + memset((char *)output.data.ptr, 0, output.data.len); + k_mutex_unlock(&output.lock); + + return ret; +} + +void scan_start(bool print) +{ + int err; + + print_scan_results = print; + num_devices_found = 0; + num_names_found = 0; + memset(ble_scanned_devices, 0, sizeof(ble_scanned_devices)); + + struct bt_le_scan_param param = { + .type = BT_LE_SCAN_TYPE_ACTIVE, + .options = BT_LE_SCAN_OPT_FILTER_DUPLICATE, + .interval = 0x0010, + .window = 0x0010, + }; + + if (!discover_in_progress) { + /* Stop the auto connect */ + bt_conn_create_auto_stop(); + + err = bt_le_scan_start(¶m, device_found); + if (err) { + LOG_ERR("Bluetooth set active scan failed " + "(err %d)\n", err); + } else { + LOG_INF("Bluetooth active scan enabled"); + + /* TODO: Get scan timeout from scan message */ + k_timer_start(&scan_timer, K_SECONDS(10), K_SECONDS(0)); + } + + scan_waiting = false; + } else { + LOG_INF("Scan waiting... Discover in progress"); + scan_waiting = true; + } +} + +static void ble_ready(int err) +{ + LOG_INF("Bluetooth ready"); + + bt_conn_cb_register(&conn_callbacks); +} + +int ble_init(void) +{ + int err; + + LOG_INF("Initializing Bluetooth.."); + k_mutex_init(&output.lock); + + err = bt_enable(ble_ready); + if (err) { + LOG_ERR("Bluetooth init failed (err %d)", err); + return err; + } + + for (int i = 0; i < MAX_SCAN_RESULTS; i++) { + memset(ble_scanned_devices[i].name, 0, + sizeof(ble_scanned_devices[1].name)); + } + + return 0; +} + +#if defined(CONFIG_BOARD_NRF9160DK_NRF9160_NS) +/* This function is missing in the Zephyr file + * boards/nordic/nrf9160dk/nrf52840_reset.c. It is now + * required by the Bluetooth stack. + */ +int bt_h4_vnd_setup(const struct device *dev) +{ + return 0; +} +#endif diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/ble.h b/samples/cellular/nrf_cloud_multi_service/src/ble/ble.h new file mode 100644 index 000000000000..885a0ccbb969 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/ble.h @@ -0,0 +1,57 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BLE_H_ +#define _BLE_H_ + +#include +#include + +#define MAX_SCAN_RESULTS 10 +#define BT_ATTR_SERVICE 0 +#define BT_ATTR_CHRC 1 +#define BT_ATTR_CCC 3 +#define BT_ADDR_LE_DEVICE_LEN 17 +#define NAME_LEN 30 +#define UUID_STR_LEN 37 +#define BT_MAX_SUBSCRIBES 25 + +struct ble_scanned_dev { + int rssi; + char name[NAME_LEN]; + char ble_mac_addr_str[BT_ADDR_LE_STR_LEN + 1]; +}; + +struct ble_device_conn; +struct desired_conn; + +typedef int (*notification_cb_t)(const char *ble_mac_addr_str, const char *chrc_uuid, + uint8_t *data, uint16_t len); + +int ble_init(void); +int ble_add_to_allowlist(const char *addr_str, bool add); +void scan_start(bool print_scan); +void ble_register_notify_callback(notification_cb_t callback); +int ble_subscribe(const char *ble_mac_addr_str, const char *chrc_uuid, uint8_t value_type); +int ble_subscribe_handle(const char *ble_mac_addr_str, uint16_t handle, uint8_t value_type); +int ble_subscribe_all(const char *ble_mac_addr_str, uint8_t value_type); +int gatt_read(const char *ble_mac_addr_str, const char *chrc_uuid, bool ccc); +int gatt_write(const char *ble_mac_addr_str, const char *chrc_uuid, uint8_t *data, + uint16_t data_len, bt_gatt_write_func_t cb); +int gatt_write_without_response(const char *ble_mac_addr_str, const char *chrc_uuid, uint8_t *data, + uint16_t data_len); +int ble_discover(struct ble_device_conn *conn_ptr); +void bt_uuid_get_str(const struct bt_uuid *uuid, char *str, size_t len); +void bt_to_upper(char *ble_mac_addr_str, uint8_t addr_len); +int disconnect_device_by_addr(const char *ble_mac_addr_str); +int device_discovery_send(struct ble_device_conn *conn_ptr); +struct ble_scanned_dev *get_scanned_device(unsigned int i); +int get_num_scan_results(void); +int get_num_scan_names(void); +void ble_stop_activity(void); +int set_shadow_desired_conn(const struct desired_conn *desired, int num_desired); +int set_shadow_ble_conn(const char *ble_address, bool connecting, bool connected); + +#endif /* _BLE_H_ */ diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/ble_codec.c b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_codec.c new file mode 100644 index 000000000000..f8ed5f6dce8c --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_codec.c @@ -0,0 +1,1315 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#undef __XSI_VISIBLE +#define __XSI_VISIBLE 1 +#include +#include +#include +#include +#if defined(CONFIG_NRF_MODEM_LIB) +#include +#endif /* CONFIG_NRF_MODEM_LIB */ +#include +#include +#include + +#include "cJSON.h" +#include "cJSON_os.h" +#include "ble_codec.h" +#include "ble_conn_mgr.h" +#include "ble.h" +#include "gateway.h" +#include "nrf_cloud_mem.h" +#include "nrf_cloud_transport.h" + +#define MAX_SERVICE_BUF_SIZE 300 + +#include +#include +LOG_MODULE_REGISTER(ble_codec, CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL); + +typedef int (*gateway_state_handler_t)(void *root_obj); + +extern void nrf_cloud_register_gateway_state_handler(gateway_state_handler_t handler); +extern int nrf_cloud_modem_info_json_encode(const struct nrf_cloud_modem_info * const mod_inf, + cJSON * const mod_inf_obj); +extern int nrf_cloud_service_info_json_encode(const struct nrf_cloud_svc_info * const svc_inf, + cJSON * const svc_inf_obj); +extern int nrf_cloud_shadow_delta_response_encode(cJSON *input_obj, + bool accept, + struct nrf_cloud_data *const output); + +extern struct ble_scanned_dev ble_scanned_devices[MAX_SCAN_RESULTS]; + +static char service_buffer[MAX_SERVICE_BUF_SIZE]; + +static bool first_service = true; +static bool first_chrc = true; +static bool desired_conns_strings; + +#define JSON_KEY_SRVC_INFO "serviceInfo" + +/* define macros to enable memory allocation error checking and + * cleanup, and also improve readability + */ +#define CJCREATE(_a_) do { \ + (_a_) = cJSON_CreateObject(); \ + if ((_a_) == NULL) { \ + goto cleanup; \ + } \ +} while (0) + +#define CJADDITEM cJSON_AddItemToObject +#define CJADDITEMCS cJSON_AddItemToObjectCS +#define CJADDREF cJSON_AddItemReferenceToObject +#define CJADDREFCS cJSON_AddItemReferenceToObjectCS + +#define CJPRINT(_a_, _b_, _c_, _d_) do { \ + int _e_ = cJSON_PrintPreallocated((_a_), (_b_), (_c_), (_d_)); \ + if (!_e_) { \ + LOG_ERR("Insufficient buffer size %d", (_c_)); \ + goto cleanup; \ + } \ +} while (0) + +#define CJCHK(_a_) do { \ + if ((_a_) == NULL) { \ + LOG_ERR("cJSON out of memory in %s", __func__); \ + goto cleanup; \ + } \ +} while (0) + +#define CJADDCHK(_a_, _b_, _c_) do { \ + CJCHK(_c_); \ + cJSON_AddItemToObject((_a_), (_b_), (_c_)); \ +} while (0) + +#define CJADDBOOL(_a_, _b_, _c_) CJCHK( \ + cJSON_AddBoolToObject((_a_), (_b_), (_c_))) +#define CJADDBOOLCS(_a_, _b_, _c_) CJCHK( \ + cJSON_AddBoolToObjectCS((_a_), (_b_), (_c_))) + +#define CJADDSTR(_a_, _b_, _c_) CJCHK( \ + cJSON_AddStringToObject((_a_), (_b_), (_c_))) +#define CJADDSTRCS(_a_, _b_, _c_) CJCHK( \ + cJSON_AddStringToObjectCS((_a_), (_b_), (_c_))) + +#define CJADDNUM(_a_, _b_, _c_) CJCHK( \ + cJSON_AddNumberToObject((_a_), (_b_), (_c_))) +#define CJADDNUMCS(_a_, _b_, _c_) CJCHK( \ + cJSON_AddNumberToObjectCS((_a_), (_b_), (_c_))) + +#define CJADDNULL(_a_, _b_) CJCHK( \ + cJSON_AddNullToObject((_a_), (_b_))) +#define CJADDNULLCS(_a_, _b_) CJCHK( \ + cJSON_AddNullToObjectCS((_a_), (_b_))) + +#define CJADDARROBJ(_a_, _b_) do { \ + (_b_) = cJSON_CreateObject(); \ + CJCHK(_b_); \ + cJSON_AddItemToArray((_a_), (_b_)); \ +} while (0) + +#define CJADDARRNUM(_a_, _b_) do { \ + cJSON *tmp = cJSON_CreateNumber((_b_)); \ + CJCHK(tmp); \ + cJSON_AddItemToArray((_a_), tmp); \ +} while (0) + +#define CJADDARRSTR(_a_, _b_) do { \ + cJSON *tmp = cJSON_CreateString((_b_)); \ + CJCHK(tmp); \ + cJSON_AddItemToArray((_a_), tmp); \ +} while (0) + + +char *get_time_str(char *dst, size_t len) +{ + int64_t unix_time_ms; + int err; + +#ifdef CONFIG_DATE_TIME + err = date_time_now(&unix_time_ms); + if (!err) { + time_t unix_time; + struct tm *time; + + unix_time = unix_time_ms / MSEC_PER_SEC; + LOG_DBG("Unix time %lld", unix_time); + time = gmtime(&unix_time); + if (time) { + /* 2020-02-19T18:38:50.363Z */ + strftime(dst, len, "%Y-%m-%dT%H:%M:%S.000Z", time); + LOG_DBG("Date/time %s", dst); + } else { + LOG_WRN("Time not valid"); + dst = NULL; + } + } else { + dst = NULL; + LOG_ERR("Date/time not available: %d", err); + } +#else + struct tm tm; + struct timespec ts; + time_t t; + + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + LOG_DBG("Unix time %lld", ts.tv_sec); + t = (time_t)ts.tv_sec; + tm = *(gmtime(&t)); + /* 2020-02-19T18:38:50.363Z */ + strftime(dst, len, "%Y-%m-%dT%H:%M:%S.000Z", &tm); + LOG_DBG("Date/time %s", dst); + } +#endif + return dst; +} + +int device_error_encode(const char *ble_address, const char *error_msg, + struct gw_msg *msg) +{ + /* TODO: Front end doesn't handle error messages yet. + * This format may change. + */ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *error = cJSON_CreateObject(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (error == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDSTRCS(root_obj, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(event, "type", "error"); + CJADDSTRCS(error, "description", error_msg); + CJADDSTRCS(device, "deviceAddress", ble_address); + + /* use references so deleting is cleaner below; must be last + * operation on any given cJSON object + */ + CJADDREFCS(event, "device", device); + CJADDREFCS(event, "error", error); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(error); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_found_encode(uint8_t num_devices_found, struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *devices = cJSON_CreateArray(); + cJSON *device = NULL; + cJSON *address = NULL; + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (devices == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "scan_result"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + CJADDSTRCS(event, "subType", "instant"); + CJADDBOOLCS(event, "timeout", true); + + for (int i = 0; i < num_devices_found; i++) { + /* TODO: Update for beacons */ + CJADDARROBJ(devices, device); + CJADDSTRCS(device, "deviceType", "BLE"); + CJADDNUMCS(device, "rssi", ble_scanned_devices[i].rssi); + if (strlen(ble_scanned_devices[i].name) > 0) { + CJADDSTRCS(device, "name", ble_scanned_devices[i].name); + } + + CJCREATE(address); + CJADDSTRCS(address, "address", ble_scanned_devices[i].ble_mac_addr_str); + CJADDITEMCS(device, "address", address); + address = NULL; + } + + /* Figure out a messageId: + * CJADDNUMCS(root_obj, "messageId", 1); + */ + CJADDREFCS(event, "devices", devices); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + if (address) { + cJSON_Delete(address); + } + cJSON_Delete(devices); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_connect_result_encode(char *ble_address, bool conn_status, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *status = cJSON_CreateObject(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (status == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "device_connect_result"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + CJADDBOOLCS(status, "connected", conn_status); + + CJADDREFCS(device, "address", address); + CJADDREFCS(device, "status", status); + CJADDREFCS(event, "device", device); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(status); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_disconnect_result_encode(char *ble_address, bool conn_status, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *status = cJSON_CreateObject(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (status == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "device_disconnect"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + CJADDBOOLCS(status, "connected", conn_status); + + CJADDREFCS(device, "status", status); + CJADDREFCS(device, "address", address); + CJADDREFCS(event, "device", device); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(status); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_value_changed_encode(char *ble_address, char *uuid, char *path, + char *value, uint16_t value_length, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *chrc = cJSON_CreateObject(); + cJSON *value_arr = cJSON_CreateArray(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (chrc == NULL) || (value_arr == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "device_characteristic_value_changed"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + /* TODO: Get Type; */ + CJADDSTRCS(address, "type", "random"); + + CJADDSTRCS(chrc, "uuid", uuid); + CJADDSTRCS(chrc, "path", path); + + for (int i = 0; i < value_length; i++) { + CJADDARRNUM(value_arr, value[i]); + } + + CJADDREFCS(device, "address", address); + CJADDREFCS(chrc, "value", value_arr); + CJADDREFCS(event, "device", device); + CJADDREFCS(event, "characteristic", chrc); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(value_arr); + cJSON_Delete(chrc); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_value_write_result_encode(const char *ble_address, const char *uuid, const char *path, + const char *value, uint16_t value_length, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *desc = cJSON_CreateObject(); + cJSON *value_arr = cJSON_CreateArray(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (desc == NULL) || (value_arr == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "device_descriptor_value_write_result"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + /* TODO: Get Type; */ + CJADDSTRCS(address, "type", "random"); + + CJADDSTRCS(desc, "uuid", uuid); + CJADDSTRCS(desc, "path", path); + + for (int i = 0; i < value_length; i++) { + CJADDARRNUM(value_arr, value[i]); + } + + CJADDREFCS(device, "address", address); + CJADDREFCS(desc, "value", value_arr); + CJADDREFCS(event, "device", device); + CJADDREFCS(event, "descriptor", desc); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(value_arr); + cJSON_Delete(desc); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_descriptor_value_encode(const char *ble_address, char *uuid, + const char *path, char *value, + uint16_t value_length, + struct gw_msg *msg, bool changed) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *desc = cJSON_CreateObject(); + cJSON *value_arr = cJSON_CreateArray(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (desc == NULL) || (value_arr == NULL)) { + LOG_ERR("Invalid parameters"); + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", changed ? + "device_descriptor_value_changed" : + "device_descriptor_value_read_result"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + /* TODO: Get Type; */ + CJADDSTRCS(address, "type", "random"); + + CJADDSTRCS(desc, "uuid", uuid); + CJADDSTRCS(desc, "path", path); + + for (int i = 0; i < value_length; i++) { + CJADDARRNUM(value_arr, value[i]); + } + + CJADDREFCS(device, "address", address); + CJADDREFCS(desc, "value", value_arr); + CJADDREFCS(event, "device", device); + CJADDREFCS(event, "descriptor", desc); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(value_arr); + cJSON_Delete(desc); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_chrc_read_encode(char *ble_address, char *uuid, char *path, + char *value, uint16_t value_length, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *chrc = cJSON_CreateObject(); + cJSON *value_arr = cJSON_CreateArray(); + char str[128]; + bool is_string = true; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (chrc == NULL) || (value_arr == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "device_characteristic_value_changed"); + /** This used to use "device_characteristic_value_read_result" + * but that no longer works in the cloud backend. + */ + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + CJADDSTRCS(address, "type", "random"); /* Not accurate: we do not know the type. */ + CJADDSTRCS(chrc, "uuid", uuid); + CJADDSTRCS(chrc, "path", path); + CJADDREFCS(device, "address", address); + + for (int i = 0; i < value_length; i++) { + if ((value[i] == '\0') && (i == (value_length - 1))) { + break; + } + if (!isprint(value[i])) { + is_string = false; + LOG_DBG("char at %d not printable: 0x%02X", i, value[i]); + break; + } + } + + if (is_string && (value_length > 1)) { + strncpy(str, value, sizeof(str) - 1); + str[MIN(sizeof(str) - 1, value_length)] = '\0'; + CJADDSTRCS(chrc, "value", str); + LOG_DBG("Value: %s", value); + } else { + LOG_HEXDUMP_DBG(value, value_length, "Value"); + } + + for (int i = 0; i < value_length; i++) { + CJADDARRNUM(value_arr, value[i]); + } + CJADDREFCS(chrc, "value", value_arr); + + CJADDREFCS(event, "device", device); + CJADDREFCS(event, "characteristic", chrc); + CJADDREFCS(root_obj, "event", event); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(value_arr); + cJSON_Delete(chrc); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +static int create_device_wrapper(char *ble_address, bool conn_status, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *event = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *address = cJSON_CreateObject(); + cJSON *status = cJSON_CreateObject(); + cJSON *services = cJSON_CreateObject(); + char str[64]; + + if ((root_obj == NULL) || (event == NULL) || (device == NULL) || + (address == NULL) || (status == NULL) || (services == NULL)) { + goto cleanup; + } + + CJADDSTRCS(root_obj, "type", "event"); + CJADDSTRCS(root_obj, "gatewayId", gateway_id); + CJADDNULLCS(root_obj, "requestId"); + + CJADDSTRCS(event, "type", "device_discover_result"); + CJADDSTRCS(event, "timestamp", get_time_str(str, sizeof(str))); + + CJADDSTRCS(device, "id", ble_address); + CJADDSTRCS(address, "address", ble_address); + CJADDBOOLCS(status, "connected", conn_status); + + CJADDREFCS(device, "status", status); + CJADDREFCS(device, "address", address); + CJADDREFCS(event, "device", device); + CJADDREFCS(event, "services", services); + CJADDREFCS(root_obj, "event", event); + + char *ptr = (char *)msg->data.ptr; + + CJPRINT(root_obj, ptr, msg->data_max_len, 0); + ptr[strlen(ptr) - 3] = 0; + msg->data.len = strlen(ptr); + ret = 0; + +cleanup: + cJSON_Delete(services); + cJSON_Delete(status); + cJSON_Delete(address); + cJSON_Delete(device); + cJSON_Delete(event); + cJSON_Delete(root_obj); + return ret; +} + +int device_shadow_data_encode(const char *ble_address, bool connecting, + bool connected, struct gw_msg *msg) +{ + __ASSERT_NO_MSG(msg != NULL); + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *state_obj = cJSON_CreateObject(); + cJSON *reported_obj = cJSON_CreateObject(); + cJSON *status_connections = cJSON_CreateObject(); + cJSON *device = cJSON_CreateObject(); + cJSON *status = cJSON_CreateObject(); + + if ((root_obj == NULL) || (state_obj == NULL) || + (reported_obj == NULL) || (status_connections == NULL) || + (device == NULL) || (status == NULL)) { + LOG_ERR("Error creating shadow data"); + goto cleanup; + } + + CJADDSTRCS(device, "id", ble_address); + CJADDBOOLCS(status, "connected", connected); + CJADDBOOLCS(status, "connecting", connecting); + + CJADDREFCS(device, "status", status); + CJADDREF(status_connections, ble_address, device); + CJADDREFCS(reported_obj, "statusConnections", status_connections); + CJADDREFCS(state_obj, "reported", reported_obj); + CJADDREFCS(root_obj, "state", state_obj); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + ret = 0; + +cleanup: + cJSON_Delete(status); + cJSON_Delete(device); + cJSON_Delete(status_connections); + cJSON_Delete(reported_obj); + cJSON_Delete(state_obj); + cJSON_Delete(root_obj); + return ret; +} + +int gateway_reported_encode(struct gw_msg *msg) +{ + __ASSERT_NO_MSG(msg != NULL); + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *state_obj = cJSON_CreateObject(); + cJSON *reported_obj = cJSON_CreateObject(); + cJSON *connections_obj = cJSON_CreateNull(); + cJSON *status_connections = cJSON_CreateNull(); + + if ((root_obj == NULL) || (state_obj == NULL) || + (reported_obj == NULL) || (connections_obj == NULL) || (status_connections == NULL)) { + goto cleanup; + } + CJADDCHK(reported_obj, "desiredConnections", connections_obj); + CJADDCHK(reported_obj, "statusConnections", status_connections); + CJADDCHK(state_obj, "reported", reported_obj); + CJADDCHK(root_obj, "state", state_obj); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + cJSON_Delete(root_obj); + return 0; + +cleanup: + cJSON_Delete(status_connections); + cJSON_Delete(connections_obj); + cJSON_Delete(reported_obj); + cJSON_Delete(state_obj); + cJSON_Delete(root_obj); + + return ret; +} + +int gateway_desired_list_encode(const struct desired_conn *desired, int num_desired, + struct gw_msg *msg) +{ + __ASSERT_NO_MSG(msg != NULL); + int ret = -ENOMEM; + cJSON *root_obj = cJSON_CreateObject(); + cJSON *state_obj = cJSON_CreateObject(); + cJSON *desired_obj = cJSON_CreateObject(); + cJSON *connections_obj = cJSON_CreateArray(); + + if ((root_obj == NULL) || (state_obj == NULL) || + (desired_obj == NULL) || (connections_obj == NULL)) { + goto cleanup; + } + + /* create array of desired BLE addresses */ + for (int i = 0; i < num_desired; i++) { + if (!desired[i].active) { + continue; + } + if (desired_conns_strings) { + /* nrfcloud stage wants the new, simpler form of + * desiredConnections where its just an array of strings + */ + CJADDARRSTR(connections_obj, desired[i].ble_mac_addr_str); + } else { + /* nrfcloud stage still uses older array of objects */ + cJSON *item; + + CJADDARROBJ(connections_obj, item); + CJADDSTRCS(item, "id", desired[i].ble_mac_addr_str); + } + } + + if (!num_desired) { + connections_obj = cJSON_CreateNull(); + } + + CJADDCHK(desired_obj, "desiredConnections", connections_obj); + CJADDCHK(state_obj, "desired", desired_obj); + CJADDCHK(root_obj, "state", state_obj); + + CJPRINT(root_obj, (char *)msg->data.ptr, msg->data_max_len, 0); + msg->data.len = strlen((char *)msg->data.ptr); + + ret = 0; + +cleanup: + cJSON_Delete(connections_obj); + cJSON_Delete(desired_obj); + cJSON_Delete(state_obj); + cJSON_Delete(root_obj); + + return ret; +} + +/* Start of functions to assemble large JSON string. No room to create these + * all at once in cJSON + */ +static int device_discover_add_service(char *discovered_json, + struct gw_msg *msg) +{ + char *ptr = (char *)msg->data.ptr; + + if (first_service == false) { + strcat(ptr, "}},"); + } + + strcat(ptr, discovered_json + 1); + + if (strlen(ptr) >= 3) { + ptr[strlen(ptr) - 3] = 0; + } + msg->data.len = strlen(ptr); + + memset(discovered_json, 0, MAX_SERVICE_BUF_SIZE); + first_service = false; + first_chrc = true; + return 0; +} + +static int device_discover_add_chrc(char *discovered_json, + struct gw_msg *msg) +{ + char *ptr = (char *)msg->data.ptr; + + if (first_chrc == false) { + strcat(ptr, ","); + } + + strcat(ptr, discovered_json + 1); + + if (strlen(ptr) >= 1) { + ptr[strlen(ptr) - 1] = 0; + } + msg->data.len = strlen(ptr); + + memset(discovered_json, 0, MAX_SERVICE_BUF_SIZE); + + first_chrc = false; + return 0; +} + +static int device_discover_add_ccc(char *discovered_json, + struct gw_msg *msg) +{ + char *ptr = (char *)msg->data.ptr; + + if (strlen(ptr) >= 2) { + ptr[strlen(ptr) - 2] = 0; + } + + strcat(ptr, discovered_json + 1); + strcat(ptr, "}"); + msg->data.len = strlen(ptr); + + memset(discovered_json, 0, MAX_SERVICE_BUF_SIZE); + + first_chrc = false; + return 0; +} + +static int svc_attr_encode(char *uuid, char *path, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *services = cJSON_CreateObject(); + cJSON *service = cJSON_CreateObject(); + cJSON *chrcs = cJSON_CreateObject(); + + if ((services == NULL) || (service == NULL) || (chrcs == NULL)) { + goto cleanup; + } + + CJADDSTRCS(service, "uuid", uuid); + + CJADDREFCS(service, "characteristics", chrcs); + CJADDREF(services, uuid, service); + + /* Print services and add to wrapper */ + CJPRINT(services, service_buffer, MAX_SERVICE_BUF_SIZE, 0); + ret = device_discover_add_service(service_buffer, msg); + +cleanup: + cJSON_Delete(chrcs); + cJSON_Delete(service); + cJSON_Delete(services); + return ret; +} + +static int chrc_attr_encode(char *uuid, char *path, struct uuid_handle_pair *h, + struct gw_msg *msg) +{ + int ret = -ENOMEM; + cJSON *chrc = cJSON_CreateObject(); + cJSON *descriptors = cJSON_CreateObject(); + cJSON *props = cJSON_CreateObject(); + cJSON *value_arr = cJSON_CreateArray(); + cJSON *parent_chrc = cJSON_CreateObject(); + uint8_t properties = h->properties; + + if ((chrc == NULL) || (descriptors == NULL) || (props == NULL) || + (value_arr == NULL) || (parent_chrc == NULL)) { + goto cleanup; + } + + CJADDSTRCS(chrc, "uuid", uuid); + CJADDSTRCS(chrc, "path", path); + + if (!h->value_len) { + CJADDARRNUM(value_arr, 0); + } else { + for (int i = 0; i < h->value_len; i++) { + CJADDARRNUM(value_arr, h->value[i]); + } + } + + /* Check and add properties */ + if (properties & BT_GATT_CHRC_READ) { + CJADDBOOLCS(props, "read", true); + } + if (properties & BT_GATT_CHRC_WRITE) { + CJADDBOOLCS(props, "write", true); + } + if (properties & BT_GATT_CHRC_INDICATE) { + CJADDBOOLCS(props, "indicate", true); + } + if (properties & BT_GATT_CHRC_NOTIFY) { + CJADDBOOLCS(props, "notify", true); + } + if (properties & BT_GATT_CHRC_WRITE_WITHOUT_RESP) { + CJADDBOOLCS(props, "writeWithoutResponse", true); + } + if (properties & BT_GATT_CHRC_AUTH) { + CJADDBOOLCS(props, "authorizedSignedWrite", true); + } + if ((properties == 0) || ((properties & + ~(BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | + BT_GATT_CHRC_INDICATE | BT_GATT_CHRC_NOTIFY | + BT_GATT_CHRC_WRITE_WITHOUT_RESP | + BT_GATT_CHRC_AUTH)) != 0)) { + LOG_ERR("Unknown CHRC property: %d\n", properties); + cJSON_Delete(parent_chrc); + return -EINVAL; + } + + CJADDREFCS(chrc, "properties", props); + CJADDREFCS(chrc, "value", value_arr); + CJADDREFCS(chrc, "descriptors", descriptors); + CJADDREF(parent_chrc, uuid, chrc); + + /* Print parent_chrhc and add to service */ + CJPRINT(parent_chrc, service_buffer, MAX_SERVICE_BUF_SIZE, 0); + ret = device_discover_add_chrc(service_buffer, msg); + +cleanup: + cJSON_Delete(parent_chrc); + cJSON_Delete(value_arr); + cJSON_Delete(props); + cJSON_Delete(descriptors); + cJSON_Delete(chrc); + return ret; +} + +static int ccc_attr_encode(char *uuid, char *path, + struct gw_msg *msg, bool sub_enabled) +{ + int ret = -ENOMEM; + cJSON *descriptor = cJSON_CreateObject(); + cJSON *value_arr = cJSON_CreateArray(); + cJSON *parent_ccc = cJSON_CreateObject(); + + if ((descriptor == NULL) || (value_arr == NULL) || + (parent_ccc == NULL)) { + goto cleanup; + } + uint8_t val = sub_enabled ? BT_GATT_CCC_NOTIFY : 0; + + CJADDSTRCS(descriptor, "uuid", uuid); + CJADDARRNUM(value_arr, val); + CJADDARRNUM(value_arr, 0); + CJADDSTRCS(descriptor, "path", path); + + CJADDREFCS(descriptor, "value", value_arr); + CJADDREF(parent_ccc, uuid, descriptor); + + /* Print parent_ccc and add to service */ + CJPRINT(parent_ccc, service_buffer, MAX_SERVICE_BUF_SIZE, 0); + ret = device_discover_add_ccc(service_buffer, msg); + +cleanup: + cJSON_Delete(parent_ccc); + cJSON_Delete(value_arr); + cJSON_Delete(descriptor); + return ret; +} + +static int attr_encode(struct uuid_handle_pair *uuid_handle, + struct uuid_handle_pair *other_handle, + char *uuid_str, char *path, + struct gw_msg *msg) +{ + int ret = 0; + + bt_to_upper(uuid_str, strlen(uuid_str)); + bt_to_upper(path, strlen(path)); + + if (uuid_handle->attr_type == BT_ATTR_SERVICE) { + LOG_INF("Encoding Service : UUID: %s", uuid_str); + ret = svc_attr_encode(uuid_str, path, msg); + + } else if (uuid_handle->attr_type == BT_ATTR_CHRC) { + ret = chrc_attr_encode(uuid_str, path, uuid_handle, msg); + + } else if (uuid_handle->attr_type == BT_ATTR_CCC) { + ret = ccc_attr_encode(uuid_str, path, msg, + uuid_handle->sub_enabled || + (other_handle ? other_handle->sub_enabled : 0)); + + } else { + LOG_ERR("Unknown Attr Type"); + ret = -EINVAL; + } + memset(service_buffer, 0, MAX_SERVICE_BUF_SIZE); + return ret; +} + +void get_uuid_str(struct uuid_handle_pair *uuid_handle, char *str, size_t len) +{ + struct bt_uuid *uuid; + + if (uuid_handle->uuid_type == BT_UUID_TYPE_16) { + uuid = &uuid_handle->uuid_16.uuid; + } else if (uuid_handle->uuid_type == BT_UUID_TYPE_128) { + uuid = &uuid_handle->uuid_128.uuid; + } else { + str[0] = '\0'; + return; + } + bt_uuid_get_str(uuid, str, len); +} + +int device_discovery_encode(struct ble_device_conn *conn_ptr, + struct gw_msg *msg) +{ + char uuid_str[BT_UUID_STR_LEN]; + char service_attr_str[BT_UUID_STR_LEN]; + char path_dep_two_str[BT_UUID_STR_LEN]; + char path_str[BT_MAX_PATH_LEN]; + int ret = 0; + uint8_t num_encoded = 0; + + first_service = true; + + LOG_INF("Num Pairs: %d", conn_ptr->num_pairs); + + ret = create_device_wrapper(conn_ptr->ble_mac_addr_str, true, msg); + if (ret) { + return ret; + } + + for (int i = 0; i < conn_ptr->num_pairs; i++) { + struct uuid_handle_pair *uuid_handle; + struct uuid_handle_pair *uh = NULL; + + uuid_handle = conn_ptr->uuid_handle_pairs[i]; + if (uuid_handle == NULL) { + continue; + } + + if ((uuid_handle->uuid_type != BT_UUID_TYPE_16) && + (uuid_handle->uuid_type != BT_UUID_TYPE_128)) { + LOG_ERR("ERROR: unknown UUID type"); + ret = -EINVAL; + continue; + } + get_uuid_str(uuid_handle, uuid_str, BT_UUID_STR_LEN); + + switch (uuid_handle->path_depth) { + case 1: + for (int j = i; j >= 0; j--) { + uh = conn_ptr->uuid_handle_pairs[j]; + if (uh == NULL) { + continue; + } + if (uh->is_service == true) { + get_uuid_str(uh, service_attr_str, + BT_UUID_STR_LEN); + break; + } + } + snprintk(path_str, BT_MAX_PATH_LEN, "%s/%s", + service_attr_str, uuid_str); + break; + case 2: + uh = conn_ptr->uuid_handle_pairs[i - 1]; + if (uh != NULL) { + get_uuid_str(uh, path_dep_two_str, + BT_UUID_STR_LEN); + snprintk(path_str, BT_MAX_PATH_LEN, "%s/%s/%s", + service_attr_str, path_dep_two_str, + uuid_str); + } + break; + } + + ret = attr_encode(uuid_handle, uh, uuid_str, path_str, msg); + if (!ret) { + num_encoded++; + } + } + + /* make sure we output at least one attribute, or + * device_discovery_send() will send malformed JSON + */ + if (num_encoded) { + /* Add the remaing brackets to the JSON string */ + strcat((char *)msg->data.ptr, "}}}}}"); + msg->data.len = strlen((char *)msg->data.ptr); + + /* ignore invalid UUID type since others were ok */ + ret = 0; + + } else if (!ret) { + ret = -EINVAL; /* no data to send */ + } + return ret; +} + +static char *get_addr_from_des_conn_array(cJSON *array, int index) +{ + cJSON *item; + cJSON *address_obj; + cJSON *ble_address; + + item = cJSON_GetArrayItem(array, index); + if (item == NULL) { + return NULL; + } + + if (item->type != cJSON_String) { + address_obj = cJSON_GetObjectItem(item, "address"); + if (address_obj == NULL) { + ble_address = cJSON_GetObjectItem(item, "id"); + } else { + ble_address = cJSON_GetObjectItem(address_obj, "address"); + } + /* the nrfcloud stage we are on uses an array of objects + * named 'address' or 'id', so remember this later if + * the shell needs to manipulate the desiredConnections + * array + */ + desired_conns_strings = false; + } else { + /* the nrfcloud stage uses a simple array of strings */ + desired_conns_strings = true; + return item->valuestring; + } + + if (ble_address != NULL) { + return ble_address->valuestring; + } + return NULL; +} + +int desired_conns_handler(cJSON *desired_connections_obj) +{ + bool changed = false; + int num_cons; + char *ble_mac_addr_str; + + if (!desired_connections_obj) { + return 0; + } + + char *ptr = cJSON_Print(desired_connections_obj); + + LOG_INF("Desired conns: %s", ptr); + nrf_cloud_free(ptr); + + if (!cJSON_GetArraySize(desired_connections_obj)) { + /* there are none, so we can't tell what format the nrfcloud + * stage wants to use... pick something here, but it may be + * wrong, which means later, if the shell user creates the first + * connection, it could be in the wrong format; but, this + * is a temporary development issue and will go away soon + * (Q1 2021) + */ + desired_conns_strings = false; + } + + struct desired_conn *cons = get_desired_array(&num_cons); + + /* check whether anything has actually changed */ + /* first, search for additions to the array */ + for (int i = 0; i < cJSON_GetArraySize(desired_connections_obj); i++) { + ble_mac_addr_str = get_addr_from_des_conn_array(desired_connections_obj, i); + + if (ble_mac_addr_str != NULL) { + bool found = false; + + for (int j = 0; j < num_cons; j++) { + if ((strcmp(ble_mac_addr_str, cons[j].ble_mac_addr_str) == 0) && + cons[j].active) { + found = true; + break; + } + } + /* new device found in cloud's array */ + if (!found) { + LOG_INF("New device added by cloud: %s", + ble_mac_addr_str); + changed = true; + break; + } + } + } + + /* second, search for removals from the array */ + for (int i = 0; i < num_cons; i++) { + bool found = false; + + if (!cons[i].active) { + continue; + } + + for (int j = 0; j < cJSON_GetArraySize(desired_connections_obj); + j++) { + ble_mac_addr_str = get_addr_from_des_conn_array( + desired_connections_obj, j); + if (ble_mac_addr_str != NULL) { + if ((strcmp(ble_mac_addr_str, cons[i].ble_mac_addr_str) == 0) && + cons[i].active) { + found = true; + break; + } + } + } + + /* device removed from cloud's array, and it wasn't manually added */ + if (!found && !cons[i].manual) { + LOG_INF("Device removed by cloud: %s", + cons[i].ble_mac_addr_str); + changed = true; + break; + } + } + + if (!changed) { +#if defined(ACK_DELTA) + int ret; + struct nrf_cloud_tx_data tx_data = { + .topic_type = NRF_CLOUD_TOPIC_STATE, + .qos = MQTT_QOS_1_AT_LEAST_ONCE + }; + + ret = nrf_cloud_shadow_delta_response_encode(state_obj, true, &tx_data.data); + + if (ret == 0) { + ret = nrf_cloud_send(&tx_data); + nrf_cloud_free((void *)tx_data.data.ptr); + if (ret) { + LOG_ERR("nct_cc_send failed %d", ret); + } + } else { + LOG_ERR("Error acking shadow delta: %d", ret); + } +#else + LOG_INF("Desired connections did not change. Ignoring delta."); +#endif + } + + LOG_DBG("Gateway state change detected"); + + ble_conn_mgr_clear_desired(false); + + for (int i = 0; i < cJSON_GetArraySize(desired_connections_obj); i++) { + + ble_mac_addr_str = get_addr_from_des_conn_array(desired_connections_obj, i); + + if (ble_mac_addr_str != NULL) { + LOG_DBG("Desired BLE address: %s", ble_mac_addr_str); + + if (!ble_conn_mgr_enabled(ble_mac_addr_str)) { + LOG_INF("Skipping disabled device: %s", + ble_mac_addr_str); + continue; + } + if (ble_conn_mgr_add_conn(ble_mac_addr_str)) { + LOG_DBG("Conn already added"); + } + ble_conn_mgr_update_desired(ble_mac_addr_str, i); + } else { + LOG_ERR("Invalid desired connection"); + return -EINVAL; + } + } + ble_conn_mgr_update_connections(); + return 0; +} + +void ble_codec_init(void) +{ + /* Nothing to do. */ +} diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/ble_codec.h b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_codec.h new file mode 100644 index 000000000000..2c63391218e4 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_codec.h @@ -0,0 +1,53 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef BLE_CODEC_H_ +#define BLE_CODEC_H_ + +#include "cJSON.h" +#include "ble_conn_mgr.h" + +struct gw_msg { + struct k_mutex lock; + int data_max_len; + struct nrf_cloud_data data; +}; + +int device_found_encode(uint8_t num_devices_found, struct gw_msg *msg); +int device_connect_result_encode(char *ble_address, bool conn_status, + struct gw_msg *msg); +int device_value_changed_encode(char *ble_address, char *uuid, char *path, + char *value, uint16_t value_length, + struct gw_msg *msg); +int device_chrc_read_encode(char *ble_address, char *uuid, char *path, + char *value, uint16_t value_length, + struct gw_msg *msg); +int device_discovery_add_attr(char *discovered_json, bool last_attr, + struct gw_msg *msg); +int device_discovery_encode(struct ble_device_conn *conn_ptr, + struct gw_msg *msg); +int device_value_write_result_encode(const char *ble_address, const char *uuid, const char *path, + const char *value, uint16_t value_length, + struct gw_msg *msg); +int device_descriptor_value_encode(const char *ble_address, char *uuid, + const char *path, char *value, + uint16_t value_length, + struct gw_msg *msg, bool changed); +int device_error_encode(const char *ble_address, const char *error_msg, + struct gw_msg *msg); +int device_disconnect_result_encode(char *ble_address, bool conn_status, + struct gw_msg *msg); +int gateway_shadow_data_encode(void *modem_ptr, struct gw_msg *msg); +int device_shadow_data_encode(const char *ble_address, bool connecting, + bool connected, struct gw_msg *msg); +int gateway_desired_list_encode(const struct desired_conn *desired, int num_desired, + struct gw_msg *msg); +int gateway_reported_encode(struct gw_msg *msg); +void get_uuid_str(struct uuid_handle_pair *uuid_handle, char *str, size_t len); +char *get_time_str(char *dst, size_t len); +void ble_codec_init(void); +int desired_conns_handler(cJSON *desired_connections_obj); + +#endif diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/ble_conn_mgr.c b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_conn_mgr.c new file mode 100644 index 000000000000..9ffb4d0e55d6 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_conn_mgr.c @@ -0,0 +1,877 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "gateway.h" +#include "ble_conn_mgr.h" +#include "ble_codec.h" +#include "cJSON.h" +#include "ble.h" +#include "cloud_connection.h" + +#include +LOG_MODULE_REGISTER(ble_conn_mgr, CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL); + +static int num_connected; +static struct ble_device_conn connected_ble_devices[CONFIG_BT_MAX_CONN]; + +static struct desired_conn desired_connections[CONFIG_BT_MAX_CONN]; + +#define CONN_MGR_STACK_SIZE 4096 +#define CONN_MGR_PRIORITY 1 + +static struct uuid_handle_pair *find_pair_by_handle(uint16_t handle, + const struct ble_device_conn *conn_ptr, + int *index); +static int ble_conn_mgr_get_free_conn(struct ble_device_conn **conn_ptr); + +static void process_connection(int i) +{ + int err; + struct ble_device_conn *dev = &connected_ble_devices[i]; + + if (dev->free) { + return; + } + + if (!await_cloud_ready(K_MSEC(50))) { + return; + } + + /* Add devices to allowlist */ + if (!dev->added_to_allowlist && !dev->shadow_updated && !dev->connected) { + if (!dev->hidden) { + err = set_shadow_ble_conn(dev->ble_mac_addr_str, true, false); + if (!err) { + dev->shadow_updated = true; + LOG_INF("Shadow updated."); + } + } + if (!ble_add_to_allowlist(dev->ble_mac_addr_str, true)) { + dev->added_to_allowlist = true; + LOG_INF("Device added to allowlist."); + } + } + + /* Connected. Do discovering if not discovered or currently + * discovering. + */ + if (dev->connected && !dev->discovered && !dev->discovering) { + + err = ble_discover(dev); + + if (!err) { + LOG_DBG("ble_discover(%s) failed: %d", + dev->ble_mac_addr_str, err); + } + } + + /* Discovering done. Encode and send. */ + if (dev->connected && dev->encode_discovered) { + dev->encode_discovered = false; + device_discovery_send(&connected_ble_devices[i]); + + bt_addr_t ble_id; + + err = bt_addr_from_str(connected_ble_devices[i].ble_mac_addr_str, + &ble_id); + if (!err) { +#if defined(CONFIG_GATEWAY_BLE_FOTA) + LOG_INF("Checking for BLE update..."); + nrf_cloud_fota_ble_update_check(&ble_id); +#endif + } + } +} + +static void connection_manager(int unused1, int unused2, int unused3) +{ + int i; + bool printed = false; + + ble_conn_mgr_init(); + while (1) { + + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + /* Manager is busy. Do nothing. */ + if (connected_ble_devices[i].discovering) { + if (!printed) { + LOG_DBG("Connection work busy."); + printed = true; + } + goto end; + } + } + printed = false; + + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + process_connection(i); + } + +end: + /* give up the CPU for a while; otherwise we spin much faster + * than the cloud side could reasonably change things, + * or that devices might connect and disconnect, and so + * would needlessly consume CPU + */ + k_sleep(K_MSEC(100)); + } +} + +K_THREAD_DEFINE(conn_mgr_thread, CONN_MGR_STACK_SIZE, + connection_manager, NULL, NULL, NULL, + CONN_MGR_PRIORITY, 0, 0); + +static void init_conn(struct ble_device_conn *dev) +{ + memset(dev, 0, sizeof(struct ble_device_conn)); + dev->free = true; +} + +static void ble_conn_mgr_conn_reset(struct ble_device_conn + *dev) +{ + struct uuid_handle_pair *uuid_handle; + + LOG_INF("Connection removed to %s", dev->ble_mac_addr_str); + + if (!dev->free) { + if (num_connected) { + num_connected--; + } + } + + /* free in backwards order to try to reduce fragmentation */ + while (dev->num_pairs) { + uuid_handle = dev->uuid_handle_pairs[dev->num_pairs - 1]; + if (uuid_handle != NULL) { + dev->uuid_handle_pairs[dev->num_pairs - 1] = NULL; + k_free(uuid_handle); + } + dev->num_pairs--; + } + init_conn(dev); +} + +void ble_conn_mgr_update_connections(void) +{ + int i; + + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + struct ble_device_conn *dev = &connected_ble_devices[i]; + + if (dev->connected || dev->added_to_allowlist) { + dev->disconnect = true; + + for (int j = 0; j < CONFIG_BT_MAX_CONN; j++) { + if (desired_connections[j].active) { + if (!strcmp(desired_connections[j].ble_mac_addr_str, + dev->ble_mac_addr_str)) { + /* If in the list then don't + * disconnect. + */ + dev->disconnect = false; + break; + } + } + } + + if (dev->disconnect) { + int err; + + LOG_INF("Cloud: disconnect device %s", + dev->ble_mac_addr_str); + if (dev->added_to_allowlist) { + if (!ble_add_to_allowlist(dev->ble_mac_addr_str, false)) { + dev->added_to_allowlist = false; + } + } + err = disconnect_device_by_addr(dev->ble_mac_addr_str); + if (err) { + LOG_ERR("Device might still be connected: %d", + err); + } + ble_conn_mgr_conn_reset(dev); + if (IS_ENABLED(CONFIG_SETTINGS)) { + LOG_INF("Saving settings"); + settings_save(); + } + } + } + } +} + +static void ble_conn_mgr_change_desired(const char *ble_mac_addr_str, uint8_t index, + bool active, bool manual) +{ + if (index <= CONFIG_BT_MAX_CONN) { + strncpy(desired_connections[index].ble_mac_addr_str, ble_mac_addr_str, + DEVICE_ADDR_LEN); + desired_connections[index].active = active; + desired_connections[index].manual = manual; + + LOG_INF("Desired Connection %s: %s %s", + active ? "Added" : "Removed", + ble_mac_addr_str, + manual ? "(manual)" : ""); + } +} + +struct desired_conn *get_desired_array(int *array_size) +{ + if (array_size == NULL) { + return NULL; + } + *array_size = ARRAY_SIZE(desired_connections); + return desired_connections; +} + +static int find_desired_connection(const char *ble_mac_addr_str, + struct desired_conn **pcon) +{ + struct desired_conn *con; + + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + con = &desired_connections[i]; + if ((strncmp(ble_mac_addr_str, con->ble_mac_addr_str, + sizeof(con->ble_mac_addr_str)) == 0) && + (ble_mac_addr_str[0] != '\0')) { + if (pcon) { + *pcon = con; + } + return i; + } + } + if (pcon) { + *pcon = NULL; + } + return -EINVAL; +} + +void ble_conn_mgr_update_desired(const char *ble_mac_addr_str, uint8_t index) +{ + ble_conn_mgr_change_desired(ble_mac_addr_str, index, true, false); +} + +int ble_conn_mgr_add_desired(const char *ble_mac_addr_str, bool manual) +{ + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (!desired_connections[i].active) { + ble_conn_mgr_change_desired(ble_mac_addr_str, i, true, manual); + return 0; + } + } + return -EINVAL; +} + +int ble_conn_mgr_rem_desired(const char *ble_mac_addr_str, bool manual) +{ + if (ble_mac_addr_str == NULL) { + return -EINVAL; + } + + int i = find_desired_connection(ble_mac_addr_str, NULL); + + if (i >= 0) { + ble_conn_mgr_change_desired(ble_mac_addr_str, i, false, manual); + return 0; + } + return -EINVAL; +} + +void ble_conn_mgr_clear_desired(bool all) +{ + LOG_INF("Desired Connections Cleared."); + + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (!desired_connections[i].manual || all) { + desired_connections[i].active = false; + } + } +} + +bool ble_conn_mgr_enabled(const char *ble_mac_addr_str) +{ + struct desired_conn *con; + + if (ble_mac_addr_str == NULL) { + return false; + } + + find_desired_connection(ble_mac_addr_str, &con); + + if (con != NULL) { + return !con->manual; + } + + return true; +} + +bool ble_conn_mgr_is_desired(const char *ble_mac_addr_str) +{ + struct desired_conn *con; + + if (ble_mac_addr_str == NULL) { + return false; + } + + find_desired_connection(ble_mac_addr_str, &con); + + return (con != NULL) && (con->active); +} + +int ble_conn_mgr_generate_path(const struct ble_device_conn *conn_ptr, + uint16_t handle, char *path, bool ccc) +{ + char path_str[BT_MAX_PATH_LEN]; + char service_uuid[BT_UUID_STR_LEN] = {0}; + char ccc_uuid[BT_UUID_STR_LEN]; + char chrc_uuid[BT_UUID_STR_LEN]; + uint8_t path_depth = 0; + struct uuid_handle_pair *uuid_handle; + int i; + + path_str[0] = '\0'; + path[0] = '\0'; + + + uuid_handle = find_pair_by_handle(handle, conn_ptr, &i); + if (uuid_handle == NULL) { + LOG_ERR("Path not generated; handle %u not found for ble_mac_addr_str %s", + handle, conn_ptr->ble_mac_addr_str); + return -ENXIO; + } + + path_depth = uuid_handle->path_depth; + + get_uuid_str(uuid_handle, chrc_uuid, BT_UUID_STR_LEN); + + if (ccc && ((i + 1) < conn_ptr->num_pairs)) { + uuid_handle = conn_ptr->uuid_handle_pairs[i + 1]; + if (uuid_handle == NULL) { + LOG_ERR("Path not generated; handle after %u " + "not found for ble_addr %s", + handle, conn_ptr->ble_mac_addr_str); + return -ENXIO; + } + get_uuid_str(uuid_handle, ccc_uuid, BT_UUID_STR_LEN); + } else { + ccc_uuid[0] = '\0'; + } + + for (int j = i; j >= 0; j--) { + uuid_handle = conn_ptr->uuid_handle_pairs[j]; + if (uuid_handle == NULL) { + continue; + } + if (uuid_handle->is_service) { + get_uuid_str(uuid_handle, service_uuid, + BT_UUID_STR_LEN); + break; + } + } + + if (!ccc) { + snprintk(path_str, BT_MAX_PATH_LEN, "%s/%s", + service_uuid, chrc_uuid); + } else { + snprintk(path_str, BT_MAX_PATH_LEN, "%s/%s/%s", + service_uuid, chrc_uuid, ccc_uuid); + } + + bt_to_upper(path_str, strlen(path_str)); + memset(path, 0, BT_MAX_PATH_LEN); + memcpy(path, path_str, strlen(path_str)); + LOG_DBG("Num pairs: %d, CCC: %d, depth: %d, generated path: %s", + conn_ptr->num_pairs, ccc, path_depth, path_str); + return 0; +} + +int ble_conn_mgr_add_conn(const char *ble_mac_addr_str) +{ + int err = 0; + struct ble_device_conn *connected_ble_ptr; + + /* Check if already added */ + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (connected_ble_devices[i].free == false) { + if (!strcmp(ble_mac_addr_str, connected_ble_devices[i].ble_mac_addr_str)) { + LOG_DBG("Connection already exsists"); + return 1; + } + } + } + + err = ble_conn_mgr_get_free_conn(&connected_ble_ptr); + + if (err) { + LOG_ERR("No free connections"); + return err; + } + + memcpy(connected_ble_ptr->ble_mac_addr_str, ble_mac_addr_str, DEVICE_ADDR_LEN); + connected_ble_ptr->free = false; + err = bt_addr_le_from_str(ble_mac_addr_str, "random", &connected_ble_ptr->bt_addr); + if (err) { + LOG_ERR("Address from string failed (err %d)", err); + } + num_connected++; + LOG_INF("BLE conn to %s added to manager", ble_mac_addr_str); + return err; +} + +int ble_conn_set_connected(struct ble_device_conn *connected_ble_ptr, bool connected) +{ + int err = 0; + + if (connected) { + connected_ble_ptr->connected = true; + } else { + connected_ble_ptr->connected = false; + connected_ble_ptr->shadow_updated = false; + } + LOG_INF("Conn updated: connected=%u", connected); + return err; +} + +int ble_conn_mgr_rediscover(const char *ble_mac_addr_str) +{ + int err = 0; + struct ble_device_conn *connected_ble_ptr; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ble_ptr); + if (err) { + LOG_ERR("Connection not found for ble_mac_addr_str %s", ble_mac_addr_str); + return err; + } + + if (!connected_ble_ptr->discovering) { + if (connected_ble_ptr->discovered) { + /* cloud wants data again; just send it */ + LOG_INF("Skipping device discovery on %s", + connected_ble_ptr->ble_mac_addr_str); + if (connected_ble_ptr->connected) { + connected_ble_ptr->encode_discovered = true; + } else { + err = device_discovery_send(connected_ble_ptr); + } + } else { + connected_ble_ptr->num_pairs = 0; + } + } + + return err; +} + +/* an nRF5 SDK device's normal MAC address least significant byte is + * incremented for the DFU bootloader MAC address + */ +int ble_conn_mgr_calc_other_addr(const char *old_addr, char *new_addr, int len, bool normal) +{ + bt_addr_le_t btaddr; + int err; + + err = bt_addr_le_from_str(old_addr, "random", &btaddr); + if (err) { + LOG_ERR("Could not convert address"); + return err; + } + if (normal) { + btaddr.a.val[0]--; + } else { + btaddr.a.val[0]++; + } + + memset(new_addr, 0, len); + bt_addr_le_to_str(&btaddr, new_addr, len); + bt_to_upper(new_addr, len); + return 0; +} + +int ble_conn_mgr_force_dfu_rediscover(const char *ble_mac_addr_str) +{ + int err = 0; + struct ble_device_conn *connected_ble_ptr; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ble_ptr); + if (err) { + LOG_ERR("Connection not found for ble_mac_addr_str %s", ble_mac_addr_str); + return err; + } + + if (!connected_ble_ptr->discovering) { + LOG_INF("Marking device %s to be rediscovered", + ble_mac_addr_str); + connected_ble_ptr->discovered = false; + connected_ble_ptr->num_pairs = 0; + } + + return 0; +} + +int ble_conn_mgr_remove_conn(const char *ble_mac_addr_str) +{ + int err = 0; + struct ble_device_conn *connected_ble_ptr; + + err = ble_conn_mgr_get_conn_by_addr(ble_mac_addr_str, &connected_ble_ptr); + if (err) { + LOG_ERR("Connection not found for ble_mac_addr_str %s", ble_mac_addr_str); + return err; + } + + ble_conn_mgr_conn_reset(connected_ble_ptr); + return err; +} + + +static int ble_conn_mgr_get_free_conn(struct ble_device_conn **conn_ptr) +{ + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (connected_ble_devices[i].free == true) { + *conn_ptr = &connected_ble_devices[i]; + LOG_DBG("Found Free connection: %d", i); + return 0; + } + } + return -ENOMEM; +} + +void ble_conn_mgr_check_pending(void) +{ + struct ble_device_conn *conn_ptr = connected_ble_devices; + + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++, conn_ptr++) { + if (conn_ptr->connected && conn_ptr->dfu_pending) { + if (conn_ptr->dfu_attempts) { + conn_ptr->dfu_attempts--; + } else { + conn_ptr->dfu_pending = false; + continue; + } + LOG_INF("Requesting pending DFU job for %s", + conn_ptr->ble_mac_addr_str); + nrf_cloud_fota_ble_update_check(&conn_ptr->bt_addr.a); + break; + } + } +} + +int ble_conn_mgr_get_conn_by_addr(const char *ble_mac_addr_str, struct ble_device_conn **conn_ptr) +{ + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (!strcmp(ble_mac_addr_str, connected_ble_devices[i].ble_mac_addr_str)) { + *conn_ptr = &connected_ble_devices[i]; + LOG_DBG("Conn Found"); + return 0; + } + } + return -ENOENT; + +} + +bool ble_conn_mgr_is_addr_connected(const char *ble_mac_addr_str) +{ + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + if (!strcmp(ble_mac_addr_str, connected_ble_devices[i].ble_mac_addr_str)) { + return connected_ble_devices[i].connected; + } + } + return false; +} + +static struct uuid_handle_pair *find_pair_by_handle(uint16_t handle, + const struct ble_device_conn *conn_ptr, + int *index) +{ + struct uuid_handle_pair *uuid_handle; + + for (int i = 0; i < conn_ptr->num_pairs; i++) { + uuid_handle = conn_ptr->uuid_handle_pairs[i]; + if (uuid_handle == NULL) { + continue; + } + if (handle == uuid_handle->handle) { + if (index != NULL) { + *index = i; + } + return uuid_handle; + } + } + return NULL; +} + +static void update_ccc_value(uint16_t handle, const struct ble_device_conn *conn_ptr, uint8_t value) +{ + struct uuid_handle_pair *ccc_handle; + + ccc_handle = find_pair_by_handle(handle + 1, conn_ptr, NULL); + if (ccc_handle && (ccc_handle->attr_type == BT_ATTR_CCC)) { + ccc_handle->value[0] = value; + ccc_handle->value_len = 2; + LOG_DBG("Updated CCC value"); + } else { + LOG_DBG("CCC not found for handle: %d", handle); + } +} + +int ble_conn_mgr_set_subscribed(uint16_t handle, uint8_t sub_index, + const struct ble_device_conn *conn_ptr) +{ + struct uuid_handle_pair *uuid_handle; + + uuid_handle = find_pair_by_handle(handle, conn_ptr, NULL); + if (uuid_handle) { + uuid_handle->sub_enabled = true; + uuid_handle->sub_index = sub_index; + update_ccc_value(handle, conn_ptr, BT_GATT_CCC_NOTIFY); + return 0; + } + + return 1; +} + + +int ble_conn_mgr_remove_subscribed(uint16_t handle, + const struct ble_device_conn *conn_ptr) +{ + struct uuid_handle_pair *uuid_handle; + + uuid_handle = find_pair_by_handle(handle, conn_ptr, NULL); + if (uuid_handle) { + uuid_handle->sub_enabled = false; + update_ccc_value(handle, conn_ptr, 0); + return 0; + } + return 1; +} + + +int ble_conn_mgr_get_subscribed(uint16_t handle, + const struct ble_device_conn *conn_ptr, + bool *status, uint8_t *sub_index) +{ + struct uuid_handle_pair *uuid_handle; + + uuid_handle = find_pair_by_handle(handle, conn_ptr, NULL); + if (uuid_handle) { + if (status) { + *status = uuid_handle->sub_enabled; + } + if (sub_index) { + *sub_index = uuid_handle->sub_index; + } + return 0; + } + + return 1; +} + +int ble_conn_mgr_get_uuid_by_handle(uint16_t handle, char *uuid, + const struct ble_device_conn *conn_ptr) +{ + char uuid_str[BT_UUID_STR_LEN]; + struct uuid_handle_pair *uuid_handle; + + memset(uuid, 0, BT_UUID_STR_LEN); + + uuid_handle = find_pair_by_handle(handle, conn_ptr, NULL); + if (uuid_handle) { + get_uuid_str(uuid_handle, uuid_str, BT_UUID_STR_LEN); + bt_to_upper(uuid_str, strlen(uuid_str)); + memcpy(uuid, uuid_str, strlen(uuid_str)); + LOG_DBG("Found UUID: %s for handle: %d", + uuid_str, + handle); + return 0; + } + + LOG_ERR("Handle %u on ble_addr %s not found; num pairs: %d", handle, + conn_ptr->ble_mac_addr_str, + (int)conn_ptr->num_pairs); + return 1; +} + +struct uuid_handle_pair *ble_conn_mgr_get_uhp_by_uuid(const char *uuid, + const struct ble_device_conn *conn_ptr) +{ + char str[BT_UUID_STR_LEN]; + + for (int i = 0; i < conn_ptr->num_pairs; i++) { + struct uuid_handle_pair *uuid_handle = conn_ptr->uuid_handle_pairs[i]; + + if (uuid_handle == NULL) { + continue; + } + + get_uuid_str(uuid_handle, str, sizeof(str)); + bt_to_upper(str, strlen(str)); + if (!strcmp(uuid, str)) { + LOG_DBG("Handle: %d, value_len: %d found for UUID: %s", + uuid_handle->handle, uuid_handle->value_len, uuid); + return uuid_handle; + } + } + + LOG_ERR("Handle not found for UUID: %s", uuid); + return NULL; +} + +int ble_conn_mgr_get_handle_by_uuid(uint16_t *handle, const char *uuid, + const struct ble_device_conn *conn_ptr) +{ + char str[BT_UUID_STR_LEN]; + + for (int i = 0; i < conn_ptr->num_pairs; i++) { + struct uuid_handle_pair *uuid_handle = + conn_ptr->uuid_handle_pairs[i]; + + if (uuid_handle == NULL) { + continue; + } + + get_uuid_str(uuid_handle, str, sizeof(str)); + bt_to_upper(str, strlen(str)); + if (!strcmp(uuid, str)) { + *handle = uuid_handle->handle; + LOG_DBG("Handle: %d found for UUID: %s", *handle, uuid); + return 0; + } + } + + LOG_ERR("Handle not found for UUID: %s", uuid); + return 1; +} + +int ble_conn_mgr_add_uuid_pair(const struct bt_uuid *uuid, uint16_t handle, + uint8_t path_depth, uint8_t properties, + uint8_t attr_type, + struct ble_device_conn *conn_ptr, + bool is_service) +{ + char str[BT_UUID_STR_LEN]; + + if (!conn_ptr) { + LOG_ERR("No connection ptr!"); + return -EINVAL; + } + + if (conn_ptr->num_pairs >= MAX_UUID_PAIRS) { + LOG_ERR("Max uuid pair limit reached on %s", + conn_ptr->ble_mac_addr_str); + return -E2BIG; + } + + LOG_DBG("Handle: %d", handle); + + if (!uuid) { + return 0; + } + + struct uuid_handle_pair *uuid_handle; + int err = 0; + + uuid_handle = conn_ptr->uuid_handle_pairs[conn_ptr->num_pairs]; + if (uuid_handle != NULL) { + /* we already discovered this device */ + if (uuid_handle->uuid_type != uuid->type) { + if (uuid_handle->uuid_type == BT_UUID_TYPE_16) { + /* likely got larger, so free and reallocate */ + k_free(uuid_handle); + uuid_handle = NULL; + } + } + } + + switch (uuid->type) { + case BT_UUID_TYPE_16: + if (uuid_handle == NULL) { + uuid_handle = (struct uuid_handle_pair *) + k_calloc(1, SMALL_UUID_HANDLE_PAIR_SIZE); + if (uuid_handle == NULL) { + LOG_ERR("Out of memory error allocating " + "for handle %u", handle); + err = -ENOMEM; + break; + } + } + memcpy(&uuid_handle->uuid_16, BT_UUID_16(uuid), + sizeof(struct bt_uuid_16)); + uuid_handle->uuid_type = uuid->type; + bt_uuid_get_str(&uuid_handle->uuid_16.uuid, str, sizeof(str)); + + LOG_DBG("\tChar: 0x%s", str); + break; + case BT_UUID_TYPE_128: + if (uuid_handle == NULL) { + uuid_handle = (struct uuid_handle_pair *) + k_calloc(1, LARGE_UUID_HANDLE_PAIR_SIZE); + if (uuid_handle == NULL) { + LOG_ERR("Out of memory error allocating " + "for handle %u", handle); + err = -ENOMEM; + break; + } + } + memcpy(&uuid_handle->uuid_128, BT_UUID_128(uuid), + sizeof(struct bt_uuid_128)); + uuid_handle->uuid_type = uuid->type; + bt_uuid_get_str(&uuid_handle->uuid_128.uuid, str, sizeof(str)); + + LOG_DBG("\tChar: 0x%s", str); + break; + default: + err = -EINVAL; + } + + if (err) { + conn_ptr->uuid_handle_pairs[conn_ptr->num_pairs] = NULL; + return err; + } + + uuid_handle->properties = properties; + uuid_handle->attr_type = attr_type; + uuid_handle->path_depth = path_depth; + uuid_handle->is_service = is_service; + uuid_handle->handle = handle; + LOG_DBG("%d. Handle: %d", conn_ptr->num_pairs, handle); + + /* finally, store it in the array of pointers to uuid_handle_pairs */ + conn_ptr->uuid_handle_pairs[conn_ptr->num_pairs] = uuid_handle; + + conn_ptr->num_pairs++; + + return 0; +} + +struct ble_device_conn *get_connected_device(unsigned int i) +{ + if (i < CONFIG_BT_MAX_CONN) { + if (!connected_ble_devices[i].free) { + return &connected_ble_devices[i]; + } + } + return NULL; +} + +int get_num_connected(void) +{ + return num_connected; +} + +void ble_conn_mgr_init(void) +{ + num_connected = 0; + for (int i = 0; i < CONFIG_BT_MAX_CONN; i++) { + init_conn(&connected_ble_devices[i]); + } +} diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/ble_conn_mgr.h b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_conn_mgr.h new file mode 100644 index 000000000000..f2018d8a4b75 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/ble_conn_mgr.h @@ -0,0 +1,113 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BLE_CONN_MGR_H_ +#define _BLE_CONN_MGR_H_ + +#include "ble.h" +#include + +#define MAX_UUID_PAIRS 68 +#define DEVICE_ADDR_LEN 18 + +#define BT_MAX_VALUE_LEN 32 +#define BT_MAX_UUID_LEN 37 +#define BT_MAX_PATH_LEN 111 + +#define MAX_DFU_ATTEMPTS 3 + +#define SMALL_UUID_HANDLE_PAIR_SIZE (sizeof(struct uuid_handle_pair) - \ + sizeof(struct bt_uuid_128) + \ + sizeof(struct bt_uuid_16)) +#define LARGE_UUID_HANDLE_PAIR_SIZE (sizeof(struct uuid_handle_pair)) + +struct uuid_handle_pair { + uint16_t handle; + uint8_t uuid_type; + uint8_t attr_type; + uint8_t path_depth; + uint8_t properties; + bool is_service; + bool sub_enabled; + uint8_t sub_index; + union { + struct bt_uuid_16 uuid_16; + struct bt_uuid_128 uuid_128; + }; + uint16_t value_len; + uint8_t value[BT_MAX_VALUE_LEN]; +}; + +struct ble_device_conn { + char ble_mac_addr_str[DEVICE_ADDR_LEN]; + bt_addr_le_t bt_addr; + struct uuid_handle_pair *uuid_handle_pairs[MAX_UUID_PAIRS]; + uint8_t num_pairs; + uint8_t dfu_attempts; + bool connected : 1; + bool discovering : 1; + bool free : 1; + bool discovered : 1; + bool encode_discovered : 1; + bool added_to_allowlist : 1; + bool shadow_updated : 1; + bool disconnect : 1; + bool dfu_pending : 1; + bool hidden : 1; +}; + +struct desired_conn { + char ble_mac_addr_str[DEVICE_ADDR_LEN]; + bool active; + bool manual; +}; + +int ble_conn_mgr_add_conn(const char *ble_mac_addr_str); +int ble_conn_mgr_calc_other_addr(const char *old_addr, char *new_addr, + int len, bool normal); +int ble_conn_mgr_generate_path(const struct ble_device_conn *conn_ptr, + uint16_t handle, + char *path, bool ccc); +int ble_conn_mgr_remove_conn(const char *ble_mac_addr_str); +int ble_conn_mgr_get_conn_by_addr(const char *ble_mac_addr_str, + struct ble_device_conn **conn_ptr); +int ble_conn_mgr_add_uuid_pair(const struct bt_uuid *uuid, uint16_t handle, + uint8_t path_depth, uint8_t properties, + uint8_t attr_type, + struct ble_device_conn *conn_ptr, + bool is_service); +int ble_conn_mgr_get_uuid_by_handle(uint16_t handle, char *uuid, + const struct ble_device_conn *conn_ptr); +int ble_conn_mgr_get_handle_by_uuid(uint16_t *handle, const char *uuid, + const struct ble_device_conn *conn_ptr); +struct uuid_handle_pair *ble_conn_mgr_get_uhp_by_uuid(const char *uuid, + const struct ble_device_conn *conn_ptr); +void ble_conn_mgr_init(void); +int ble_conn_set_connected(struct ble_device_conn *conn_ptr, bool connected); +int ble_conn_mgr_set_subscribed(uint16_t handle, uint8_t sub_index, + const struct ble_device_conn *conn_ptr); +int ble_conn_mgr_remove_subscribed(uint16_t handle, + const struct ble_device_conn *conn_ptr); +int ble_conn_mgr_get_subscribed(uint16_t handle, + const struct ble_device_conn *conn_ptr, + bool *status, uint8_t *sub_index); +void ble_conn_mgr_update_desired(const char *ble_mac_addr_str, uint8_t index); +int ble_conn_mgr_add_desired(const char *ble_mac_addr_str, bool manual); +int ble_conn_mgr_rem_desired(const char *ble_mac_addr_str, bool manual); +bool ble_conn_mgr_is_desired(const char *ble_mac_addr_str); +void ble_conn_mgr_clear_desired(bool all); +bool ble_conn_mgr_enabled(const char *ble_mac_addr_str); +void ble_conn_mgr_update_connections(void); +int ble_conn_mgr_rediscover(const char *ble_mac_addr_str); +struct ble_device_conn *get_connected_device(unsigned int i); +int get_num_connected(void); +struct desired_conn *get_desired_array(int *array_size); +bool ble_conn_mgr_is_addr_connected(const char *ble_mac_addr_str); +void ble_conn_mgr_print_mem(void); +int ble_conn_mgr_find_related_addr(const char *old_addr, char *new_addr, int len); +int ble_conn_mgr_force_dfu_rediscover(const char *ble_mac_addr_str); +void ble_conn_mgr_check_pending(void); + +#endif diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/cli.c b/samples/cellular/nrf_cloud_multi_service/src/ble/cli.c new file mode 100644 index 000000000000..126758cb22ca --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/cli.c @@ -0,0 +1,1679 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#undef __XSI_VISIBLE +#define __XSI_VISIBLE 1 +#undef _XOPEN_SOURCE +#define _XOPEN_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(CONFIG_NRF_CLOUD_FOTA) +#include +#endif +#include +#include +#include +#include +#include +#include +#include "ncs_version.h" +#include "nrf_cloud_transport.h" +#include "ble.h" +#include "ble_codec.h" +#include "ble_conn_mgr.h" +#include "peripheral_dfu.h" +#include "gateway.h" +#include "flash_test.h" + +LOG_MODULE_REGISTER(cli, CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL); + +#define DEFAULT_PASSWORD CONFIG_SHELL_DEFAULT_PASSWORD + +/* disable for now -- breaks BLE HCI after used */ +#define PRINT_CTLR_INFO_ENABLED 0 + +/* set to 1 to enable showing cert list */ +#define DISPLAY_CERT_LIST 0 + +#ifndef CONFIG_LOG_DOMAIN_ID +#define CONFIG_LOG_DOMAIN_ID 0 +#endif + +enum ble_cmd_type { + BLE_CMD_ALL, + BLE_CMD_MAC, + BLE_CMD_NAME +}; + +struct modem_param_info modem_param; + +/* was: CONFIG_AT_CMD_RESPONSE_MAX_LEN]; */ +static char response_buf[2048]; + +static void set_at_prompt(const struct shell *shell, bool at_mode); +void peripheral_dfu_set_test_mode(bool test); + +char *rem_eol(const char *s, char *d, int len) +{ + if (!s) { + return ""; + } + + char *p = d; + + strncpy(d, s, len); + + while (*p) { + if ((*p == '\r') || (*p == '\n')) { + *p = '\0'; + break; + } + p++; + } + return d; +} + +void cmd_testflash(const struct shell *shell) +{ + int ret; + + if (IS_ENABLED(CONFIG_FLASH_TEST)) { + shell_print(shell, "Testing flash..."); + ret = flash_test(); + } else { + shell_print(shell, "Test not enabled"); + ret = -ENOTSUP; + } + + if (ret) { + shell_error(shell, "Error testing flash: %d", ret); + } else { + shell_print(shell, "Test passed."); + } +} + +void print_fw_info(const struct shell *shell, bool verbose) +{ + extern struct fw_info m_firmware_info; + struct fw_info *info = &m_firmware_info; + + if (info) { + shell_print(shell, "Board name: \t%s", CONFIG_BOARD); + shell_print(shell, "App name: \t%s", STRINGIFY(PROJECT_NAME)); + shell_print(shell, "App rev: \t%s", CONFIG_APP_VERSION); + shell_print(shell, "nRF Connect SDK rev:\t%s", STRINGIFY(NCS_BUILD_VERSION)); + shell_print(shell, "Zephyr rev: \t%s", STRINGIFY(BUILD_VERSION)); + shell_print(shell, "Built date and time:\t%s %s", __DATE__, __TIME__); + if (!verbose) { + return; + } + if (!fw_info_check((uint32_t)info)) { + shell_error(shell, "fw_info struct is invalid"); + return; + } + shell_print(shell, "Ver: \t%u", info->version); + shell_print(shell, "Size: \t%u", info->size); + shell_print(shell, "Start: \t0x%08x", info->address); + shell_print(shell, "Boot: \t0x%08x", info->boot_address); + } +} + +#if defined(CONFIG_SYS_HEAP_RUNTIME_STATS) +extern struct sys_heap _system_heap; +#endif + +int heap_shell(const struct shell *shell, size_t argc, char **argv) +{ +#if defined(CONFIG_SYS_HEAP_RUNTIME_STATS) + int err; + struct sys_memory_stats kernel_stats; + + err = sys_heap_runtime_stats_get(&_system_heap, &kernel_stats); + if (err) { + shell_error(shell, "heap: failed to read kernel heap statistics, error: %d", err); + } else { + shell_print(shell, "kernel heap statistics:"); + shell_print(shell, "free: %6d", kernel_stats.free_bytes); + shell_print(shell, "allocated: %6d", kernel_stats.allocated_bytes); + shell_print(shell, "max. allocated: %6d\n", kernel_stats.max_allocated_bytes); + } +#endif /* CONFIG_SYS_HEAP_RUNTIME_STATS */ + +/* Calculate the system heap maximum size. */ +#define USED_RAM_END_ADDR POINTER_TO_UINT(&_end) +#define HEAP_BASE USED_RAM_END_ADDR +#define MAX_HEAP_SIZE (KB(CONFIG_SRAM_SIZE) - (HEAP_BASE - CONFIG_SRAM_BASE_ADDRESS)) + + shell_print(shell, "system heap statistics:"); + shell_print(shell, "max. size: %6ld", MAX_HEAP_SIZE); + + return 0; +} + +void print_modem_info(const struct shell *shell, bool creds) +{ +#ifdef CONFIG_MODEM_INFO + modem_info_init(); + modem_info_params_init(&modem_param); + + int ret = modem_info_params_get(&modem_param); + + if (ret) { + shell_error(shell, "Error getting modem info: %d", ret); + } else { + struct device_param *dev = &modem_param.device; + struct network_param *net = &modem_param.network; + struct sim_param *sim = &modem_param.sim; + char buf[128]; + + if (dev->modem_fw.type == MODEM_INFO_FW_VERSION) { + shell_print(shell, "Modem fw: \t%s", + rem_eol(dev->modem_fw.value_string, buf, sizeof(buf))); + } else { + shell_print(shell, "Modem fw type: \t%u val %u", + dev->modem_fw.type, + dev->modem_fw.value); + } + if (dev->battery.type == MODEM_INFO_BATTERY) { + shell_print(shell, "Battery: \t%u mV", + dev->battery.value); + } + if (dev->imei.type == MODEM_INFO_IMEI) { + shell_print(shell, "IMEI: \t\t%s", + rem_eol(dev->imei.value_string, buf, sizeof(buf))); + } + shell_print(shell, "Board: \t\t%s", + rem_eol(dev->board, buf, sizeof(buf))); + shell_print(shell, "App Name: \t%s", + rem_eol(dev->app_name, buf, sizeof(buf))); + shell_print(shell, "App Ver: \t%s", + rem_eol(dev->app_version, buf, sizeof(buf))); + + if (sim->uicc.type == MODEM_INFO_UICC) { + shell_print(shell, "UICC: \t\t%u", + sim->uicc.value); + } + if (sim->iccid.type == MODEM_INFO_ICCID) { + shell_print(shell, "ICCID: \t\t%s", + rem_eol(sim->iccid.value_string, buf, sizeof(buf))); + } + if (sim->imsi.type == MODEM_INFO_IMSI) { + shell_print(shell, "IMSI: \t\t%s", + rem_eol(sim->imsi.value_string, buf, sizeof(buf))); + } + + if (net->current_band.type == + MODEM_INFO_CUR_BAND) { + shell_print(shell, "Cur band: \t%u", + net->current_band.value); + } + if (net->sup_band.type == + MODEM_INFO_SUP_BAND) { + shell_print(shell, "Sup band: \t%s", + rem_eol(net->sup_band.value_string, buf, sizeof(buf))); + } + if (net->area_code.type == + MODEM_INFO_AREA_CODE) { + shell_print(shell, "Area code: \t%s", + rem_eol(net->area_code.value_string, buf, sizeof(buf))); + } + if (net->current_operator.type == + MODEM_INFO_OPERATOR) { + shell_print(shell, "Operator: \t%s", + rem_eol(net->current_operator.value_string, buf, sizeof(buf))); + } + if (net->mcc.type == + MODEM_INFO_MCC) { + shell_print(shell, "MCC: \t\t%u", + net->mcc.value); + } + if (net->mnc.type == + MODEM_INFO_MNC) { + shell_print(shell, "MNC: \t\t%u", + net->mnc.value); + } + if (net->ip_address.type == + MODEM_INFO_IP_ADDRESS) { + shell_print(shell, "IP address: \t%s", + rem_eol(net->ip_address.value_string, buf, sizeof(buf))); + } + if (net->ue_mode.type == + MODEM_INFO_UE_MODE) { + shell_print(shell, "UE mode: \t%u", + net->ue_mode.value); + } + if (net->apn.type == + MODEM_INFO_APN) { + shell_print(shell, "Access point: \t%s", + rem_eol(net->apn.value_string, buf, sizeof(buf))); + } + if (net->lte_mode.value == 1) { + shell_print(shell, "Mode: \t\tLTE-M"); + } else if (net->nbiot_mode.value == 1) { + shell_print(shell, "Mode: \t\tNB-IoT"); + } + shell_print(shell, "Cell ID: \t%ld", + (long)net->cellid_dec); + +#if defined(CONFIG_MODEM_INFO_ADD_DATE_TIME) || defined(CONFIG_DATE_TIME_MODEM) + char *str = net->date_time.value_string; + struct timespec now; + struct tm *tm; + char *atime; + + shell_print(shell, "Net date/time: \t%s " + "DST %d TZ %ld", + str, _daylight, _timezone); + clock_gettime(CLOCK_REALTIME, &now); + tm = localtime(&now.tv_sec); + if (tm == NULL) { + shell_error(shell, "could not get local time"); + return; + } + atime = asctime(tm); + if (atime == NULL) { + shell_error(shell, "could not get asctime"); + return; + } + shell_print(shell, "Time now: \t%s", asctime(tm)); +#else + char time_str[30] = {0}; + + if (get_time_str(time_str, sizeof(time_str))) { + shell_print(shell, "Current UTC: \t%s", time_str); + } +#endif + } +#endif + + if (creds) { + response_buf[0] = '\0'; + int err; + + err = nrf_modem_at_cmd(response_buf, sizeof(response_buf), "%s", "AT%CMNG=1"); + if (err < 0) { + shell_error(shell, "ERROR: %d", err); + } else if (err > 0) { + shell_error(shell, "ERROR: 0x%X", nrf_modem_at_err(err)); + } + + shell_print(shell, "%s", response_buf); + } +} + +void print_connection_status(const struct shell *shell) +{ + int err; + enum lte_lc_nw_reg_status status; + enum lte_lc_system_mode smode; + enum lte_lc_system_mode_preference smode_pref; + enum lte_lc_func_mode fmode; + + err = lte_lc_nw_reg_status_get(&status); + if (err) { + shell_error(shell, "lte_lc_nw_reg_status_get: %d", err); + } else { + shell_print(shell, "LTE reg status: \t%d", (int)status); + } + err = lte_lc_system_mode_get(&smode, &smode_pref); + if (err) { + shell_error(shell, "lte_lc_system_mode_get: %d", err); + } else { + shell_print(shell, "LTE system mode: \t%d", (int)smode); + shell_print(shell, "system mode pref: \t%d", (int)smode_pref); + } + err = lte_lc_func_mode_get(&fmode); + if (err) { + shell_error(shell, "lte_lc_func_mode_get: %d", err); + } else { + shell_print(shell, "LTE functional mode: \t%d", (int)fmode); + } +} + +static void print_cloud_info(const struct shell *shell) +{ + char stage[NRF_CLOUD_STAGE_ID_MAX_LEN] = "unknown"; + char tenant_id[NRF_CLOUD_TENANT_ID_MAX_LEN] = "unknown"; + static char id[NRF_CLOUD_CLIENT_ID_MAX_LEN] = "unknown"; + + nct_stage_get(stage, sizeof(stage)); + nrf_cloud_tenant_id_get(tenant_id, sizeof(tenant_id)); + nrf_cloud_client_id_get(id, sizeof(id)); + print_connection_status(shell); + + shell_print(shell, "nrfcloud stage: \t%s", stage); + shell_print(shell, "nrfcloud endpoint: \t%s", CONFIG_NRF_CLOUD_HOST_NAME); + shell_print(shell, "nrfcloud tenant id: \t%s", tenant_id); + shell_print(shell, "nrfcloud dev id: \t%s", id); + shell_print(shell, "security sectag: \t%d", CONFIG_NRF_CLOUD_SEC_TAG); +} + +static void print_ctlr_info(const struct shell *shell) +{ +#if PRINT_CTLR_INFO_ENABLED + struct net_buf *rsp; + int ret; + + ret = bt_hci_cmd_send_sync(BT_HCI_OP_VS_READ_VERSION_INFO, NULL, &rsp); + if (ret) { + shell_error(shell, "Could not read version info: %d", ret); + } else { + struct bt_hci_rp_vs_read_version_info *info; + + info = (void *)rsp->data; + shell_print(shell, "HW platform: \t0x%04x", info->hw_platform); + shell_print(shell, "HW variant: \t0x%04x", info->hw_variant); + shell_print(shell, "FW variant: \t0x%02x", info->fw_variant); + shell_print(shell, "FW version: \t%u.%u", + info->fw_version, + sys_le16_to_cpu(info->fw_revision)); + shell_print(shell, "Build: \t0x%08X", info->fw_build); + } + + if (!IS_ENABLED(CONFIG_BT_LL_SOFTDEVICE)) { + ret = bt_hci_cmd_send_sync(BT_HCI_OP_VS_READ_BUILD_INFO, + NULL, &rsp); + if (ret) { + LOG_ERR("Could not read build info: %d", ret); + } else { + struct bt_hci_rp_vs_read_build_info *build; + + build = (void *)rsp->data; + shell_print(shell, "OS build: \t%s", + (const char *)build->info); + } + } + + if (IS_ENABLED(CONFIG_BT_LL_SOFTDEVICE)) { + ret = bt_hci_cmd_send_sync(BT_HCI_OP_VS_READ_STATIC_ADDRS, + NULL, &rsp); + if (ret || (rsp == NULL)) { + LOG_ERR("Could not read static address: %d", ret); + } else { + struct bt_hci_rp_vs_read_static_addrs *addrs; + uint8_t *a; + + addrs = (void *)rsp->data; + a = &addrs->a[0].bdaddr.val[0]; + shell_print(shell, "Static addr: \t" + "%02X:%02X:%02X:%02X:%02X:%02X", + a[0], a[1], a[2], a[3], a[4], a[5]); + } + } +#else + shell_print(shell, "unavailable"); +#endif +} + +static void print_scan_info(const struct shell *shell) +{ + unsigned int i; + struct ble_scanned_dev *dev; + + shell_print(shell, " MAC, type, RSSI, name"); + for (i = 0; i < MAX_SCAN_RESULTS; i++) { + dev = get_scanned_device(i); + if (dev == NULL) { + shell_print(shell, ""); + break; + } + shell_print(shell, "%u. %s, %d, %s", + i + 1, dev->ble_mac_addr_str, dev->rssi, dev->name); + } +} + +static void print_conn_info(const struct shell *shell, bool show_path, + bool notify) +{ + unsigned int i; + unsigned int j; + unsigned int k; + int count = 0; + struct ble_device_conn *dev; + struct uuid_handle_pair *up; + char uuid_str[BT_UUID_STR_LEN]; + char path[BT_MAX_PATH_LEN]; + char props[64]; + static const char * const types[] = {"svc", "chr", "---", "ccc"}; + static const char * const properties[] = {"brdcst", "read", "wrnorsp", "write", + "notif", "indi", "auth", "extprop"}; + + if (!notify) { + shell_print(shell, " MAC, connected, discovered, shadow" + " updated, denylist status, ctrld by," + " visible, num UUIDs"); + } + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + dev = get_connected_device(i); + if (dev == NULL) { + continue; + } + count++; + shell_print(shell, "%u. %s, %s, %s, %s, %s, %s, %s, UUIDs:%u", + count, + dev->ble_mac_addr_str, + dev->connected ? "CONNNECTED" : "disconnected", + dev->discovered ? "DISCOVERED" : + (dev->discovering ? "DISCOVERING" : "not dscvred"), + dev->shadow_updated ? "SHADOW UPDATED" : + "shadow not set", + dev->added_to_allowlist ? "CONN ALLOWED" : + "conn denied", + ble_conn_mgr_enabled(dev->ble_mac_addr_str) ? "CLOUD" : "local", + !dev->hidden ? "VISIBLE" : "hidden", + (unsigned int)dev->num_pairs + ); + if (!notify) { + shell_print(shell, " is service, UUID, UUID type, " + "handle, type, path depth, " + "properties, sub index, sub " + "enabled, val_len"); + } + for (j = 0; j < dev->num_pairs; j++) { + up = dev->uuid_handle_pairs[j]; + if (up == NULL) { + continue; + } + if (notify && !up->sub_enabled) { + continue; + } + get_uuid_str(up, uuid_str, BT_UUID_STR_LEN); + props[0] = '\0'; + for (k = 0; k < 8; k++) { + if (up->properties & (1 << k)) { + if (props[0]) { + strncat(props, ", ", sizeof(props) - 1); + } + strncat(props, properties[k], sizeof(props) - 1); + } + } + shell_print(shell, + " %u, %s, %s, %u, %u, %s, %u, 0x%02X (%s), %u, %s, %u", + j + 1, + up->is_service ? "serv" : "char", + uuid_str, + up->uuid_type, + up->handle, + up->attr_type <= BT_ATTR_CCC ? types[up->attr_type] : "unk", + (unsigned int)up->path_depth, + (unsigned int)up->properties, + props, + (unsigned int)up->sub_index, + up->sub_enabled ? "NOTIFY ON" : "notify off", + up->value_len + ); + if (up->value_len) { + shell_hexdump(shell, up->value, up->value_len); + } + if (show_path) { + ble_conn_mgr_generate_path(dev, up->handle, path, + up->attr_type == BT_ATTR_CCC); + shell_print(shell, " %u, %s", + up->handle, path); + } + } + } +} + +static void print_irq_info(const struct shell *shell) +{ + int i; + extern char _vector_start[]; + void **vectors = (void *)_vector_start; + + shell_print(shell, "IRQn, entry, prio, en, pend, active, arg"); + for (i = -15; i < IRQ_TABLE_SIZE; i++) { + void *entry; + const void *arg; + + if (i < 0) { + entry = vectors[i + 16]; + arg = 0; + } else { + entry = _sw_isr_table[i].isr; + arg = _sw_isr_table[i].arg; + } + if (entry == z_irq_spurious) { + continue; + } + shell_print(shell, "% 3d. %10p, %d, %d, %d, %d, %10p", + i, entry, + NVIC_GetPriority(i), NVIC_GetEnableIRQ(i), + NVIC_GetPendingIRQ(i), NVIC_GetActive(i), + arg); + } +} + +static int ble_conn_save(const struct shell *shell) +{ + int num; + struct desired_conn *desired = get_desired_array(&num); + + return set_shadow_desired_conn(desired, num); +} + +static int ble_conn_mac(const struct shell *shell, char *ble_mac_addr_str) +{ + int err; + + shell_print(shell, " Adding connection to %s...", ble_mac_addr_str); + err = ble_conn_mgr_add_conn(ble_mac_addr_str); + if (!err) { + shell_print(shell, " Adding to desired list..."); + err = ble_conn_mgr_add_desired(ble_mac_addr_str, true); + } + if (err) { + shell_error(shell, " Failed: error %d", err); + } else { + shell_print(shell, " Done."); + } + + return err; +} + +static int ble_conn_all(const struct shell *shell) +{ + unsigned int i; + struct ble_scanned_dev *dev; + int err = 0; + int count = 0; + + for (i = 0; i < MAX_SCAN_RESULTS; i++) { + dev = get_scanned_device(i); + if (dev == NULL) { + break; + } + err = ble_conn_mac(shell, dev->ble_mac_addr_str); + if (!err) { + count++; + } + } + + shell_print(shell, "Connected to %d devices", count); + return err; +} + +static int ble_conn_name(const struct shell *shell, char *name) +{ + unsigned int i; + struct ble_scanned_dev *dev; + + for (i = 0; i < MAX_SCAN_RESULTS; i++) { + dev = get_scanned_device(i); + if (dev == NULL) { + break; + } + if (strcmp(name, dev->name) == 0) { + shell_print(shell, "Connecting to %s", name); + return ble_conn_mac(shell, dev->ble_mac_addr_str); + } + } + + shell_error(shell, "BLE device %s not found", name); + return -EINVAL; +} + +static int ble_disconn_mac(const struct shell *shell, char *ble_mac_addr_str) +{ + int err; + + shell_print(shell, " Disconnecting device..."); + if (!ble_conn_mgr_is_addr_connected(ble_mac_addr_str)) { + err = ble_add_to_allowlist(ble_mac_addr_str, false); + if (err) { + shell_error(shell, " Failed to remove from allowlist: %d", err); + } + } + err = disconnect_device_by_addr(ble_mac_addr_str); + if (err) { + shell_error(shell, " Error disconnecting device: %d", err); + } + shell_print(shell, " Removing connection to %s...", ble_mac_addr_str); + err = ble_conn_mgr_remove_conn(ble_mac_addr_str); + if (!err) { + shell_print(shell, " Removing from desired list..."); + err = ble_conn_mgr_rem_desired(ble_mac_addr_str, true); + if (err) { + shell_error(shell, " Failed to remove from desired list: %d", err); + } + } + if (err) { + shell_error(shell, " Failed: error %d", err); + } else { + shell_print(shell, " Done."); + } + + return err; +} + +static int ble_disconn_all(const struct shell *shell) +{ + unsigned int i; + struct ble_device_conn *con; + int err = 0; + int count = 0; + + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + con = get_connected_device(i); + if (con == NULL) { + continue; + } + err = ble_disconn_mac(shell, con->ble_mac_addr_str); + if (!err) { + count++; + } + } + + shell_print(shell, "Disconnected %d devices", count); + return 0; +} + +static int ble_disconn_name(const struct shell *shell, char *name) +{ + unsigned int i; + struct ble_scanned_dev *dev; + + for (i = 0; i < MAX_SCAN_RESULTS; i++) { + dev = get_scanned_device(i); + if (dev == NULL) { + break; + } + if (strcmp(name, dev->name) == 0) { + shell_print(shell, "Disconnecting %s", name); + return ble_disconn_mac(shell, dev->ble_mac_addr_str); + } + } + + shell_print(shell, "BLE device %s not found", name); + return -EINVAL; +} + +static enum ble_cmd_type get_cmd_type(char *arg) +{ + enum ble_cmd_type ret; + + if (strcmp(arg, "all") == 0) { + ret = BLE_CMD_ALL; + } else if (strchr(arg, ':')) { + ret = BLE_CMD_MAC; + } else { + ret = BLE_CMD_NAME; + } + return ret; +} + +/* This dynamic command helper is called repeatedly by the shell when + * the user wants command completion which lists possible MAC addresses. + * It calls this until it returns NULL. + * + * The first for loop returns all the devices we are already supposed to + * connect to, because they're in the shadow's desiredConnections array. + * The second nested for loop effectively appends to that list any scan + * results which are not also devices we should connect with (so those + * devices are not listed twice). + * + * This would happen if the user used the CLI to scan for advertising + * devices with 'ble scan', then used 'ble conn' to connect to one + * (which temporarily adds this new device to the desiredConnections + * array), and then used a dynamic command completion which hits this + * function. + * + * Often, the scan results are empty, and there is just a short list + * of desired connections, so the second loop does nothing. + */ +static const char *get_mac_addr(size_t idx, bool all) +{ + unsigned int i; + unsigned int j; + struct ble_scanned_dev *dev; + struct ble_device_conn *con; + int count = 0; + + /* combine our list of desired connections and scan results */ + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + con = get_connected_device(i); + if (con == NULL) { + continue; + } + if (count == idx) { + return con->ble_mac_addr_str; + } + count++; + } + + if (!all) { + return NULL; + } + + for (i = 0; i < MAX_SCAN_RESULTS; i++) { + dev = get_scanned_device(i); + if (dev == NULL) { + break; /* end of list of scanned devices */ + } + for (j = 0; j < CONFIG_BT_MAX_CONN; j++) { + con = get_connected_device(i); + if (con == NULL) { + continue; + } + if (strcmp(con->ble_mac_addr_str, dev->ble_mac_addr_str) == 0) { + dev = NULL; + break; + } + } + if (dev == NULL) { + continue; /* scan matches connected device, so skip */ + } + if (count == idx) { + return dev->ble_mac_addr_str; + } + count++; + } + + return NULL; +} + +static const char *get_device_name(size_t idx) +{ + unsigned int i; + struct ble_scanned_dev *dev; + int count = 0; + + for (i = 0; i < MAX_SCAN_RESULTS; i++) { + dev = get_scanned_device(i); + if (dev == NULL) { + break; /* end of list of scanned devices */ + } + if (strlen(dev->name) == 0) { + continue; + } + if (count == idx) { + return dev->name; + } + count++; + } + + return NULL; +} + +static const char *get_cmd_param(size_t idx) +{ + /* all is a valid parameter */ + if (idx == 0) { + return "all"; + } + /* return any known device names */ + if (idx <= get_num_scan_names()) { + return get_device_name(idx - 1); + } + /* then any MAC addresses */ + return get_mac_addr(idx - 1 - get_num_scan_names(), true); +} + +uint32_t get_log_module_level(const struct shell *shell, const char *name) +{ + uint32_t modules_cnt = log_src_cnt_get(CONFIG_LOG_DOMAIN_ID); + const char *tmp_name; + uint32_t i; + uint32_t level = LOG_LEVEL_NONE; + + /* if current log level of ble module >= INF, then no print needed */ + for (i = 0U; i < modules_cnt; i++) { + tmp_name = log_source_name_get(CONFIG_LOG_DOMAIN_ID, i); + if (tmp_name == NULL) { + continue; + } + if (strcmp(tmp_name, name) == 0) { + level = log_filter_get(shell->log_backend->backend, + CONFIG_LOG_DOMAIN_ID, i, true); + break; + } + } + return level; +} + +/* COMMAND HANDLERS */ + +static int cmd_info_list(const struct shell *shell, size_t argc, char **argv) +{ + /* Get MAC from argv */ + shell_print(shell, "MAC selected: %s", argv[0]); + + size_t idx = 0; + const char *ble_mac_addr_str; + + for (idx = 0;; idx++) { + ble_mac_addr_str = get_mac_addr(idx, true); + if (ble_mac_addr_str) { + shell_print(shell, "%zd. %s", idx, ble_mac_addr_str); + } else { + shell_print(shell, "end of list"); + break; + } + } + return 0; +} + +#define DYNAMIC_ADDR_HELP " Valid BLE MAC address" + +static void get_dynamic_addr(size_t idx, struct shell_static_entry *entry) +{ + entry->syntax = get_mac_addr(idx, true); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_info_list; + entry->subcmd = NULL; + entry->help = DYNAMIC_ADDR_HELP; + entry->args.mandatory = 1; + entry->args.optional = 6; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_addr, get_dynamic_addr); + +static int cmd_param_list(const struct shell *shell, size_t argc, char **argv) +{ + /* Get MAC from argv */ + shell_print(shell, "param selected: %s", argv[0]); + + size_t idx = 0; + const char *param; + + shell_print(shell, "num scan names: %d", get_num_scan_names()); + shell_print(shell, "num scan results: %d", get_num_scan_results()); + shell_print(shell, "num connected: %d", get_num_connected()); + + for (idx = 0;; idx++) { + param = get_cmd_param(idx); + if (param) { + shell_print(shell, "%zd. %s", idx, param); + } else { + shell_print(shell, "end of list"); + break; + } + } + return 0; +} + +#define DYNAMIC_PARAM_HELP " all | name | MAC" + +static void get_dynamic_param(size_t idx, struct shell_static_entry *entry) +{ + entry->syntax = get_cmd_param(idx); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_param_list; + entry->subcmd = NULL; + entry->help = DYNAMIC_PARAM_HELP; + entry->args.mandatory = 1; + entry->args.optional = 6; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_param, get_dynamic_param); + +static int cmd_info_gateway(const struct shell *shell, size_t argc, char **argv) +{ + bool detailed = false; + + if (argc > 1) { + if (strcmp(argv[1], "verbose") == 0) { + detailed = true; + } + } + + print_fw_info(shell, true); + return 0; +} + +static int cmd_info_modem(const struct shell *shell, size_t argc, char **argv) +{ + bool creds = false; + + if (argc > 1) { + if (strcmp(argv[1], "verbose") == 0) { + creds = true; + } + } + print_modem_info(shell, creds); + return 0; +} + +static int cmd_info_cloud(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + print_cloud_info(shell); + return 0; +} + +static int cmd_info_ctlr(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + print_ctlr_info(shell); + return 0; +} + +static int cmd_info_scan(const struct shell *shell, size_t argc, + char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + print_scan_info(shell); + return 0; +} + +static int cmd_info_conn(const struct shell *shell, size_t argc, + char **argv) +{ + bool path = false; + bool notify = false; + int i; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "path") == 0) { + path = true; + } else if (strcmp(argv[i], "notify") == 0) { + notify = true; + } else { + shell_error(shell, "unknown option: %s", argv[i]); + return 0; + } + } + print_conn_info(shell, path, notify); + return 0; +} + +static int cmd_info_irq(const struct shell *shell, size_t argc, + char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + print_irq_info(shell); + return 0; +} + +#if CONFIG_GATEWAY_BLE_FOTA +static int cmd_ble_test(const struct shell *shell, size_t argc, char **argv) +{ + static bool test_mode = true; + + shell_print(shell, "Setting BLE FOTA test mode to %u", test_mode); + peripheral_dfu_set_test_mode(test_mode); + test_mode = !test_mode; + return 0; +} +#endif + +static int cmd_ble_scan(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + /* if current log level of ble module < INF, then print needed */ + bool print_scan = (get_log_module_level(shell, "ble") < LOG_LEVEL_INF); + + scan_start(print_scan); + return 0; +} + +static int cmd_ble_save(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + shell_print(shell, "save connection list to cloud..."); + return ble_conn_save(shell); +} + +static int cmd_ble_conn(const struct shell *shell, size_t argc, char **argv) +{ + char *arg = argv[0]; + + if (argc < 1) { + return -EINVAL; + } + + switch (get_cmd_type(arg)) { + case BLE_CMD_ALL: + shell_print(shell, "connecting all BLE devices..."); + return ble_conn_all(shell); + case BLE_CMD_MAC: + shell_print(shell, "connecting to MAC %s", arg); + return ble_conn_mac(shell, arg); + case BLE_CMD_NAME: + shell_print(shell, "connecting to name %s", arg); + return ble_conn_name(shell, arg); + default: + return -EINVAL; + } +} + +static void get_dynamic_ble_conn(size_t idx, struct shell_static_entry *entry) +{ + entry->syntax = get_cmd_param(idx); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_ble_conn; + entry->subcmd = NULL; + entry->help = DYNAMIC_PARAM_HELP; + entry->args.mandatory = 1; + entry->args.optional = 0; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_ble_conn, get_dynamic_ble_conn); + +static int cmd_ble_disc(const struct shell *shell, size_t argc, char **argv) +{ + char *arg = argv[0]; + + if (argc < 1) { + return -EINVAL; + } + + switch (get_cmd_type(arg)) { + case BLE_CMD_ALL: + shell_print(shell, "disconnecting all BLE devices..."); + return ble_disconn_all(shell); + case BLE_CMD_MAC: + shell_print(shell, "disconnecting MAC %s", arg); + return ble_disconn_mac(shell, arg); + case BLE_CMD_NAME: + shell_print(shell, "disconnecting name %s", arg); + return ble_disconn_name(shell, arg); + default: + return -EINVAL; + } +} + +static void get_dynamic_ble_disc(size_t idx, struct shell_static_entry *entry) +{ + entry->syntax = get_cmd_param(idx); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_ble_disc; + entry->subcmd = NULL; + entry->help = DYNAMIC_PARAM_HELP; + entry->args.mandatory = 1; + entry->args.optional = 0; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_ble_disc, get_dynamic_ble_disc); + +static int cmd_ble_notif_en(const struct shell *shell, size_t argc, char **argv) +{ + char *arg = argv[0]; + + if (argc < 1) { + return -EINVAL; + } + + switch (get_cmd_type(arg)) { + case BLE_CMD_ALL: + shell_print(shell, "enable notifications on all devices"); + struct ble_device_conn *dev; + int i; + int count = 0; + + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + dev = get_connected_device(i); + if (dev == NULL) { + continue; + } + shell_print(shell, "enabling on MAC %s...", dev->ble_mac_addr_str); + ble_subscribe_all(dev->ble_mac_addr_str, BT_GATT_CCC_NOTIFY); + count++; + } + if (!count) { + shell_warn(shell, "Connect a device first."); + } + break; + case BLE_CMD_MAC: + if ((argc < 2) || (strcmp(argv[1], "all") == 0)) { + shell_print(shell, "enable all notifications on MAC %s", + arg); + ble_subscribe_all(arg, BT_GATT_CCC_NOTIFY); + return 0; + } + uint16_t handle = atoi(argv[1]); + + shell_print(shell, "enable notification on MAC %s handle %u", + arg, handle); + ble_subscribe_handle(arg, handle, BT_GATT_CCC_NOTIFY); + break; + default: + return -EINVAL; + } + return 0; +} + +static void get_dynamic_ble_en(size_t idx, struct shell_static_entry *entry) +{ + entry->syntax = get_cmd_param(idx); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_ble_notif_en; + entry->subcmd = NULL; + entry->help = DYNAMIC_PARAM_HELP; + entry->args.mandatory = 1; + entry->args.optional = 2; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_ble_en, get_dynamic_ble_en); + +static int cmd_ble_notif_dis(const struct shell *shell, size_t argc, char **argv) +{ + char *arg = argv[0]; + + if (argc < 1) { + return -EINVAL; + } + + switch (get_cmd_type(arg)) { + case BLE_CMD_ALL: + shell_print(shell, "disable notifications on all devices"); + struct ble_device_conn *dev; + int i; + + for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { + dev = get_connected_device(i); + if (dev == NULL) { + continue; + } + shell_print(shell, "disabling on MAC %s...", + dev->ble_mac_addr_str); + ble_subscribe_all(dev->ble_mac_addr_str, 0); + } + break; + case BLE_CMD_MAC: + if ((argc < 2) || (strcmp(argv[1], "all") == 0)) { + shell_print(shell, "disable all notifications on MAC %s", + arg); + ble_subscribe_all(arg, 0); + return 0; + } + uint16_t handle = atoi(argv[1]); + + shell_print(shell, "disable notification on MAC %s handle %u", + arg, handle); + ble_subscribe_handle(arg, handle, 0); + break; + default: + return -EINVAL; + } + return 0; +} + +static void get_dynamic_ble_dis(size_t idx, struct shell_static_entry *entry) +{ + entry->syntax = get_cmd_param(idx); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_ble_notif_dis; + entry->subcmd = NULL; + entry->help = DYNAMIC_PARAM_HELP; + entry->args.mandatory = 1; + entry->args.optional = 1; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_ble_dis, get_dynamic_ble_dis); + +static void set_at_prompt(const struct shell *shell, bool at_mode) +{ + static bool normal_prompt = true; + + if (at_mode && normal_prompt) { + normal_prompt = false; + + shell_print(shell, "Type 'exit' to exit AT mode"); + shell_prompt_change(shell, ""); + + shell_use_colors_set(shell, false); + + } else if (!at_mode && !normal_prompt) { + normal_prompt = true; + + shell_prompt_change(shell, CONFIG_SHELL_PROMPT_SECURE); + + /* hack: manually turn off select mode as is done in + * zephyr/subsys/shell/shell.c:alt_metakeys_handle() + * when it receives SHELL_VT100_ASCII_ALT_R -- there is + * no API for doing so + */ + shell_set_root_cmd(NULL); + + shell_use_colors_set(shell, true); + } +} + +static int app_cmd_at(const struct shell *shell, size_t argc, char **argv) +{ + if (argv[1] == NULL) { + shell_help(shell); + return -EINVAL; + } + if (strcmp(argv[1], "enable") == 0) { + shell_set_root_cmd("at"); + set_at_prompt(shell, true); + return 0; + } else if (strcmp(argv[1], "exit") == 0) { + set_at_prompt(shell, false); + return 0; + } + int err; + + if (strcmp(argv[1], "AT+CFUN=4") == 0) { + control_cloud_connection(false); /* disable reconnections */ + } else if (strcmp(argv[1], "AT+CFUN=1") == 0) { + control_cloud_connection(true); /* enable reconnections */ + } + + char *c = argv[1]; + int i = 0; + + while ((c = strstr(c, "\\n")) != NULL) { + c[0] = '\r'; + c[1] = '\n'; + i++; + } + + err = nrf_modem_at_cmd(response_buf, sizeof(response_buf), "%s", argv[1]); + if (err) { + shell_error(shell, "ERROR"); + return -EINVAL; + } + + shell_print(shell, "%s", response_buf); + + return 0; +} + +static int app_exit(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + set_at_prompt(shell, false); + return 0; +} + +static int cmd_reboot(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + device_shutdown(true); + return 0; +} + +static int cmd_shutdown(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + device_shutdown(false); + return 0; +} + +/* + * example: + * fota firmware.nrfcloud.com 3c7003c6-45a0-4a74-9023-1e006ceeb835/APP*30f6ce17*1.4.0/app_update.bin + */ +static int cmd_fota(const struct shell *shell, size_t argc, char **argv) +{ +#if defined(CONFIG_NRF_CLOUD_FOTA) + if (argc < 3) { + return -EINVAL; + } + char *host = argv[1]; + char *path = argv[2]; + int sec_tag = CONFIG_NRF_CLOUD_SEC_TAG; + size_t frag = CONFIG_NRF_CLOUD_FOTA_DOWNLOAD_FRAGMENT_SIZE; + + if (argc > 3) { + sec_tag = atoi(argv[3]); + } + if (argc > 4) { + frag = atoll(argv[4]); + } + shell_print(shell, "starting FOTA download from host:%s, path:%s, " + "sec_tag:%d, frag_size:%zd", + host, path, sec_tag, frag); + + char *space = strstr(path, " "); + + if (space) { + shell_print(shell, "mcuboot download detected"); + } + int err = fota_download_start(host, path, sec_tag, 0, frag); + + if (err) { + shell_error(shell, "Error %d starting download", err); + } + return 0; +#else + shell_error(shell, "FOTA support not enabled in this build."); + return 0; +#endif +} + +/* + * example: + * ble fota C2:6B:AC:6D:05:A3 firmware.beta.nrfcloud.com ba1752ef-0d36-4fcf-8748-3cad9f8801b0/ + * APP*f1078fc9*2.2.0/app_thingy_s132.bin 155272 1 2.2.0 + * ble fota C2:6B:AC:6D:05:A3 firmware.beta.nrfcloud.com ba1752ef-0d36-4fcf-8748-3cad9f8801b0/ + * APP*56693cf8*2.2.0/app_thingy_s132dat.bin 135 1 2.2.0 + * ble fota C2:6B:AC:6D:05:A3 firmware.beta.nrfcloud.com ba1752ef-0d36-4fcf-8748-3cad9f8801b0/ + * APP*f15b85a8*s132/sd_bl.bin 153344 0 s132 + * ble fota C2:6B:AC:6D:05:A3 firmware.beta.nrfcloud.com ba1752ef-0d36-4fcf-8748-3cad9f8801b0/ + * APP*3fb54637*s132/sd_bl_dat.bin 139 1 s132 + */ +#if CONFIG_GATEWAY_BLE_FOTA +static int cmd_ble_fota(const struct shell *shell, size_t argc, char **argv) +{ + if (argc < 3) { + return -EINVAL; + } + char *addr = argv[0]; + char *host = argv[1]; + char *path = argv[2]; + int size = 0; + bool init_packet = true; + char *ver = "1"; + uint32_t crc = 0; + int sec_tag = -1; + char *apn = NULL; + size_t frag = 0; + + if (argc > 3) { + size = atoi(argv[3]); + } + if (argc > 4) { + init_packet = atoi(argv[4]) != 0; + } + if (argc > 5) { + ver = argv[5]; + } + if (argc > 6) { + crc = atoi(argv[6]); + } + if (argc > 7) { + sec_tag = atoi(argv[7]); + } + if (argc > 8) { + frag = atoll(argv[8]); + } + if (argc > 9) { + apn = argv[9]; + } + + shell_print(shell, "starting BLE DFU to addr:%s, from host:%s, " + "path:%s, size:%d, final:%d, ver:%s, crc:%u, " + "sec_tag:%d, apn:%s, frag_size:%zd", + addr, host, path, size, init_packet, ver, crc, + sec_tag, apn ? apn : "", frag); + + /* set parameters for BLE update */ + int err; + + err = peripheral_dfu_config(addr, size, ver, crc, init_packet, true); + if (err) { + peripheral_dfu_cleanup(); + shell_error(shell, "Error %d starting peripheral DFU", err); + } else { + err = peripheral_dfu_start(host, path, sec_tag, apn, frag); + if (err) { + shell_error(shell, "Error %d starting download", err); + } + } + return 0; +} + +static void get_dynamic_ble_fota(size_t idx, struct shell_static_entry *entry) +{ + /* build dynamic list of mac addresses of only devices we have + * connected with (not scan results too) + */ + entry->syntax = get_mac_addr(idx, false); + + if (entry->syntax == NULL) { + return; + } + + entry->handler = cmd_ble_fota; + entry->subcmd = NULL; + entry->help = DYNAMIC_PARAM_HELP; + entry->args.mandatory = 5; + entry->args.optional = 5; +} + +SHELL_DYNAMIC_CMD_CREATE(dynamic_ble_fota, get_dynamic_ble_fota); +#endif + + +static int cmd_session(const struct shell *shell, size_t argc, char **argv) +{ + if (argc < 2) { + shell_print(shell, "Persistent sessions = %d", + nct_get_session_state()); + } else { + int flag = atoi(argv[1]); + + if ((nct_get_session_state() == 0) && (flag == 1)) { + shell_warn(shell, "Setting persistent sessions true " + "when it is not, may result " + "in data loss; use at your own " + "risk."); + } + shell_print(shell, "Setting persistent sessions = %d", flag); + nct_save_session_state(flag); + } + return 0; +} + +int check_passwd(char *passwd) +{ + return strcmp(passwd, DEFAULT_PASSWORD); +} + +int is_valid_passwd(char *passwd) +{ + if (strlen(passwd) < 8) { + return -EINVAL; + } else { + return 0; + } +} + +int set_passwd(char *passwd) +{ + return -ENOTSUP; +} + +static int cmd_login(const struct shell *shell, size_t argc, char **argv) +{ + static uint32_t attempts; + + if (argc < 2) { + shell_print(shell, "Access requires: "); + return -EINVAL; + } + + if (check_passwd(argv[1]) == 0) { + attempts = 0; + shell_obscure_set(shell, false); + z_shell_log_backend_enable(shell->log_backend, (void *)shell, + CONFIG_STARTING_LOG_LEVEL); + z_shell_history_purge(shell->history); + shell_set_root_cmd(NULL); + shell_prompt_change(shell, CONFIG_SHELL_PROMPT_SECURE); + + shell_print(shell, "\nnRF Cloud Gateway\n"); + print_fw_info(shell, false); + + shell_print(shell, "\nHit tab for help.\n"); + return 0; + } + shell_error(shell, "Incorrect password!"); + attempts++; + if (attempts > 3) { + k_sleep(K_SECONDS(attempts)); + } + return -EINVAL; +} + +static int cmd_passwd(const struct shell *shell, size_t argc, char **argv) +{ + int err; + + err = check_passwd(argv[1]); + if (err) { + shell_error(shell, "Incorrect password!"); + return err; + } + + err = is_valid_passwd(argv[2]); + if (err) { + shell_error(shell, "Invalid password. Must be 8 characters or longer."); + return err; + } + + err = set_passwd(argv[2]); + if (!err) { + shell_print(shell, "Password changed."); + } else { + shell_error(shell, "Unable to store password."); + } + + return err; +} + +static int cmd_logout(const struct shell *shell, size_t argc, char **argv) +{ + shell_set_root_cmd("login"); + shell_obscure_set(shell, true); + shell_prompt_change(shell, CONFIG_SHELL_PROMPT_UART); + shell_print(shell, "\n"); + return 0; +} + +void cli_init(void) +{ + const struct shell *shell = shell_backend_uart_get_ptr(); + + if (shell) { + /* + * Zephyr commit: SHA-1: 2b5723d4558619a9283683499c095e0b344d32d7 + * shell: add init backend configuration + * broke the config system regarding obscure, so for now, + * set it manually on + */ + shell_obscure_set(shell, IS_ENABLED(CONFIG_SHELL_START_OBSCURED)); + } + + if (IS_ENABLED(CONFIG_SHELL_START_OBSCURED)) { + shell_set_root_cmd("login"); + } else { + shell_prompt_change(shell, CONFIG_SHELL_PROMPT_SECURE); + } + +#if defined(CONFIG_STARTING_LOG_OVERRIDE) + for (int i = 0; i < log_src_cnt_get(CONFIG_LOG_DOMAIN_ID); i++) { + if (IS_ENABLED(CONFIG_NRF_CLOUD_GATEWAY_LOG_LEVEL_DBG)) { + printk("%d. %s\n", i, + log_source_name_get(CONFIG_LOG_DOMAIN_ID, i)); + } + log_filter_set(NULL, CONFIG_LOG_DOMAIN_ID, i, + CONFIG_STARTING_LOG_LEVEL); + } +#endif +} + +SHELL_STATIC_SUBCMD_SET_CREATE(sub_info, + SHELL_CMD(cloud, NULL, "Cloud information.", cmd_info_cloud), + SHELL_CMD(ctlr, NULL, "BLE controller information.", cmd_info_ctlr), + SHELL_CMD(conn, NULL, "[path] [notify] Connected Bluetooth devices information.", + cmd_info_conn), + SHELL_CMD(gateway, NULL, " Gateway information.", cmd_info_gateway), + SHELL_CMD(heap, NULL, "Print heap usage statistics.", heap_shell), + SHELL_COND_CMD(CONFIG_GATEWAY_DBG_CMDS, irq, NULL, "Dump IRQ table.", cmd_info_irq), + SHELL_COND_CMD(CONFIG_GATEWAY_DBG_CMDS, list, &dynamic_addr, + "List known BLE MAC addresses.", NULL), + SHELL_CMD(modem, NULL, " Modem information.", cmd_info_modem), + SHELL_COND_CMD(CONFIG_GATEWAY_DBG_CMDS, param, &dynamic_param, "List parameters.", NULL), + SHELL_CMD(scan, NULL, "Bluetooth scan results.", cmd_info_scan), + SHELL_SUBCMD_SET_END /* Array terminated. */ +); +SHELL_CMD_REGISTER(info, &sub_info, "Informational commands", NULL); + +SHELL_STATIC_SUBCMD_SET_CREATE(sub_ble, + SHELL_CMD(scan, NULL, "Scan for BLE devices.", cmd_ble_scan), + SHELL_CMD(save, NULL, "Save desired connections to shadow.", cmd_ble_save), + SHELL_CMD(conn, &dynamic_ble_conn, " Connect to BLE device(s).", NULL), + SHELL_CMD(disc, &dynamic_ble_disc, " Disconnect BLE device(s).", NULL), + SHELL_CMD(en, &dynamic_ble_en, " Enable " + "notifications on BLE device(s).", NULL), + SHELL_CMD(dis, &dynamic_ble_dis, " Disable " + "notifications on BLE device(s).", NULL), +#if CONFIG_GATEWAY_BLE_FOTA + SHELL_CMD(fota, &dynamic_ble_fota, + " [ver] [crc] [sec_tag] [frag_size] [apn] " + "BLE firmware over-the-air update.", NULL), + SHELL_CMD(test, NULL, "Set BLE FOTA download test mode.", cmd_ble_test), +#endif + SHELL_SUBCMD_SET_END /* Array terminated. */ +); +SHELL_CMD_ARG_REGISTER(ble, &sub_ble, "Bluetooth commands", NULL, 0, 3); + +SHELL_CMD_ARG_REGISTER(fota, NULL, " [sec_tag] [frag_size] [apn] " + "firmware over-the-air update.", cmd_fota, 2, 2); +SHELL_CMD_ARG_REGISTER(at, NULL, " | exit> Execute an AT " + "command. Use first to remain " + "in AT command mode until 'exit'.", + app_cmd_at, 2, SHELL_OPT_ARG_RAW); +SHELL_CMD_ARG_REGISTER(session, NULL, "<0 | 1> Get or change persistent sessions flag.", + cmd_session, 0, 1); +SHELL_CMD_REGISTER(testflash, NULL, "Test the external flash.", cmd_testflash); +SHELL_CMD_REGISTER(reboot, NULL, "Reboot the gateway.", cmd_reboot); +SHELL_CMD_REGISTER(shutdown, NULL, "Shutdown the gateway.", cmd_shutdown); +SHELL_CMD_REGISTER(exit, NULL, "Exit 'select at' mode.", app_exit); +SHELL_CMD_ARG_REGISTER(login, NULL, "", cmd_login, 2, 0); +SHELL_CMD_ARG_REGISTER(passwd, NULL, " ", cmd_passwd, 3, 0); +SHELL_CMD_REGISTER(logout, NULL, "Log out.", cmd_logout); diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/dfu/peripheral_dfu.c b/samples/cellular/nrf_cloud_multi_service/src/ble/dfu/peripheral_dfu.c new file mode 100644 index 000000000000..fe3048cf87bc --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/dfu/peripheral_dfu.c @@ -0,0 +1,1362 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nrf_cloud_fota.h" +#include "ble.h" +#include "gateway.h" +#include "ble_conn_mgr.h" +#include "peripheral_dfu.h" + +LOG_MODULE_REGISTER(peripheral_dfu, CONFIG_NRF_CLOUD_FOTA_LOG_LEVEL); + +#define DFU_CONTROL_POINT_UUID "8EC90001F3154F609FB8838830DAEA50" +#define DFU_PACKET_UUID "8EC90002F3154F609FB8838830DAEA50" +#define DFU_BUTTONLESS_UUID "8EC90003F3154F609FB8838830DAEA50" +#define DFU_BUTTONLESS_BONDED_UUID "8EC90004F3154F609FB8838830DAEA50" +#define MAX_CHUNK_SIZE 20 +#define PROGRESS_UPDATE_INTERVAL 5 +#define MAX_FOTA_FILES 2 + +#define CALL_TO_PRINTK(fmt, ...) do { \ + printk(fmt "\n", ##__VA_ARGS__); \ + } while (false) + +#define LOGPKINF(...) do { \ + if (use_printk) { \ + CALL_TO_PRINTK(__VA_ARGS__); \ + } else { \ + LOG_INF(__VA_ARGS__); \ + } \ + } while (false) + +enum nrf_dfu_op_t { + NRF_DFU_OP_PROTOCOL_VERSION = 0x00, + NRF_DFU_OP_OBJECT_CREATE = 0x01, + NRF_DFU_OP_RECEIPT_NOTIF_SET = 0x02, + NRF_DFU_OP_CRC_GET = 0x03, + NRF_DFU_OP_OBJECT_EXECUTE = 0x04, + NRF_DFU_OP_OBJECT_SELECT = 0x06, + NRF_DFU_OP_MTU_GET = 0x07, + NRF_DFU_OP_OBJECT_WRITE = 0x08, + NRF_DFU_OP_PING = 0x09, + NRF_DFU_OP_HARDWARE_VERSION = 0x0A, + NRF_DFU_OP_FIRMWARE_VERSION = 0x0B, + NRF_DFU_OP_ABORT = 0x0C, + NRF_DFU_OP_RESPONSE = 0x60, + NRF_DFU_OP_INVALID = 0xFF +}; + +enum nrf_dfu_result_t { + NRF_DFU_RES_CODE_INVALID = 0x00, + NRF_DFU_RES_CODE_SUCCESS = 0x01, + NRF_DFU_RES_CODE_OP_CODE_NOT_SUPPORTED = 0x02, + NRF_DFU_RES_CODE_INVALID_PARAMETER = 0x03, + NRF_DFU_RES_CODE_INSUFFICIENT_RESOURCES = 0x04, + NRF_DFU_RES_CODE_INVALID_OBJECT = 0x05, + NRF_DFU_RES_CODE_UNSUPPORTED_TYPE = 0x07, + NRF_DFU_RES_CODE_OPERATION_NOT_PERMITTED = 0x08, + NRF_DFU_RES_CODE_OPERATION_FAILED = 0x0A, + NRF_DFU_RES_CODE_EXT_ERROR = 0x0B +}; + +typedef enum { + NRF_DFU_EXT_ERROR_NO_ERROR = 0x00, + NRF_DFU_EXT_ERROR_INVALID_ERROR_CODE = 0x01, + NRF_DFU_EXT_ERROR_WRONG_COMMAND_FORMAT = 0x02, + NRF_DFU_EXT_ERROR_UNKNOWN_COMMAND = 0x03, + NRF_DFU_EXT_ERROR_INIT_COMMAND_INVALID = 0x04, + NRF_DFU_EXT_ERROR_FW_VERSION_FAILURE = 0x05, + NRF_DFU_EXT_ERROR_HW_VERSION_FAILURE = 0x06, + NRF_DFU_EXT_ERROR_SD_VERSION_FAILURE = 0x07, + NRF_DFU_EXT_ERROR_SIGNATURE_MISSING = 0x08, + NRF_DFU_EXT_ERROR_WRONG_HASH_TYPE = 0x09, + NRF_DFU_EXT_ERROR_HASH_FAILED = 0x0A, + NRF_DFU_EXT_ERROR_WRONG_SIGNATURE_TYPE = 0x0B, + NRF_DFU_EXT_ERROR_VERIFICATION_FAILED = 0x0C, + NRF_DFU_EXT_ERROR_INSUFFICIENT_SPACE = 0x0D, +} nrf_dfu_ext_error_code_t; + +enum nrf_dfu_firmware_type_t { + NRF_DFU_FIRMWARE_TYPE_SOFTDEVICE = 0x00, + NRF_DFU_FIRMWARE_TYPE_APPLICATION = 0x01, + NRF_DFU_FIRMWARE_TYPE_BOOTLOADER = 0x02, + NRF_DFU_FIRMWARE_TYPE_UNKNOWN = 0xFF +}; + +struct dfu_notify_packet { + uint8_t magic; + uint8_t op; + uint8_t result; + union { + struct select { + uint32_t max_size; + uint32_t offset; + uint32_t crc; + } select; + struct crc { + uint32_t offset; + uint32_t crc; + } crc; + struct hw_version { + uint32_t part; + uint32_t variant; + uint32_t rom_size; + uint32_t ram_size; + uint32_t rom_page_size; + } hw; + struct fw_version { + uint8_t type; + uint32_t version; + uint32_t addr; + uint32_t len; + } fw; + uint8_t ext_err_code; + }; +} __packed; + +enum ble_dfu_buttonless_op_code_t { + DFU_OP_RESERVED = 0x00, + DFU_OP_ENTER_BOOTLOADER = 0x01, + DFU_OP_SET_ADV_NAME = 0x02, + DFU_OP_RESPONSE_CODE = 0x20 +}; + +enum ble_dfu_buttonless_rsp_code_t { + DFU_RSP_INVALID = 0x00, + DFU_RSP_SUCCESS = 0x01, + DFU_RSP_OP_CODE_NOT_SUPPORTED = 0x02, + DFU_RSP_OPERATION_FAILED = 0x04, + DFU_RSP_ADV_NAME_INVALID = 0x05, + DFU_RSP_BUSY = 0x06, + DFU_RSP_NOT_BONDED = 0x07 +}; + +struct secure_dfu_ind_packet { + uint8_t resp_code; + uint8_t op_code; + uint8_t rsp_code; +} __packed; + +K_SEM_DEFINE(peripheral_dfu_active, 1, 1); +static size_t download_size; +static size_t completed_size; +static bool finish_page; +static bool test_mode; +static bool verbose; +static uint32_t max_size; +static uint32_t flash_page_size; +static uint32_t page_remaining; + +/* responses from peripheral device */ +static bool notify_received; +static bool normal_mode; +static uint16_t notify_length; +static uint8_t dfu_notify_data[MAX_CHUNK_SIZE]; + +/* normal mode BLE device address */ +static char ble_norm_addr[BT_ADDR_STR_LEN]; +/* normal mode secure DFU responses */ +struct secure_dfu_ind_packet *secure_dfu_ind_packet_data = + (struct secure_dfu_ind_packet *) dfu_notify_data; + +/* DFU mode BLE device address */ +static char ble_dfu_addr[BT_ADDR_STR_LEN]; +struct ble_device_conn *dfu_conn_ptr; +/* DFU mode protocol responses */ +struct dfu_notify_packet *dfu_notify_packet_data = + (struct dfu_notify_packet *)dfu_notify_data; + +static struct nrf_cloud_fota_ble_job fota_ble_job; +static struct nrf_cloud_fota_job_info fota_files[MAX_FOTA_FILES]; +static int total_completed_size; +static int active_num; +static int num_fota_files; +static struct k_work_delayable fota_job_work; + +static char app_version[16]; +static int image_size; +static uint16_t original_crc; +static int socket_retries_left; +static bool first_fragment; +static bool init_packet; +static bool use_printk; +static struct download_client dlc; + +static int send_select_command(char *ble_addr); +static int send_create_command(char *ble_addr, uint32_t size); +static int send_switch_to_dfu(char *ble_addr); +static int send_select_data(char *ble_addr); +static int send_create_data(char *ble_addr, uint32_t size); +static int send_prn(char *ble_addr, uint16_t receipt_rate); +static int send_data(char *ble_addr, const char *buf, size_t len); +static int send_request_crc(char *ble_addr); +static int send_execute(char *ble_addr); +static int send_abort(char *ble_addr); +static void on_sent(struct bt_conn *conn, uint8_t err, + struct bt_gatt_write_params *params); +static int send_hw_version_get(char *ble_addr); +static int send_fw_version_get(char *ble_addr, uint8_t fw_type); +static uint8_t peripheral_dfu(const char *buf, size_t len); +static int download_client_callback(const struct download_client_evt *event); +static void cancel_dfu(enum nrf_cloud_fota_error error); +static void fota_ble_callback(const struct nrf_cloud_fota_ble_job * + const ble_job); +static void free_job(void); +static bool start_next_job(void); +static void fota_job_next(struct k_work *work); + +void peripheral_dfu_set_test_mode(bool test) +{ + test_mode = test; +} + +static int notification_callback(const char *addr, const char *chrc_uuid, + uint8_t *data, uint16_t len) +{ + if ((strcmp(addr, ble_norm_addr) != 0) && + (strcmp(addr, ble_dfu_addr) != 0)) { + LOG_WRN("Notification received for %s (not us)", addr); + return 0; /* not for us */ + } + /** @TODO: implement bonded mode also */ + if (strcmp(chrc_uuid, DFU_BUTTONLESS_UUID) == 0) { + normal_mode = true; + } else if (strcmp(chrc_uuid, DFU_CONTROL_POINT_UUID) == 0) { + normal_mode = false; + } else { + LOG_WRN("Notification received for wrong UUID:%s", chrc_uuid); + return 0; /* not for us */ + } + notify_length = MIN(len, sizeof(dfu_notify_data)); + memcpy(dfu_notify_data, data, notify_length); + notify_received = true; + LOG_DBG("%sdfu notified %u bytes", normal_mode ? "secure " : "", notify_length); + LOG_HEXDUMP_DBG(data, notify_length, "notify packet"); + return 1; +} + +static int wait_for_notification(void) +{ + int max_loops = 1000; + + while (!notify_received) { + k_sleep(K_MSEC(10)); + if (!max_loops--) { + return -ETIMEDOUT; + } + } + return 0; +} + +static int decode_secure_dfu(void) +{ + struct secure_dfu_ind_packet *p = secure_dfu_ind_packet_data; + int err; + + if (notify_length < 3) { + LOG_ERR("Indication too short: %u < 3", notify_length); + return -EINVAL; + } + if (p->resp_code != DFU_OP_RESPONSE_CODE) { + LOG_ERR("First byte not 0x%02X: 0x%02X", DFU_OP_RESPONSE_CODE, p->resp_code); + return -EINVAL; + } + switch (p->op_code) { + case DFU_OP_ENTER_BOOTLOADER: + if (p->rsp_code == DFU_RSP_SUCCESS) { + LOG_INF("Device switching to DFU mode!"); + err = 0; + } else { + LOG_ERR("Error %d switching to DFU mode", p->rsp_code); + err = -EIO; + } + break; + case DFU_OP_SET_ADV_NAME: + if (p->rsp_code == DFU_RSP_SUCCESS) { + LOG_INF("DFU adv name changed successfully"); + err = 0; + } else { + LOG_ERR("Error %d changing DFU adv name", p->rsp_code); + err = -EIO; + } + break; + default: + LOG_ERR("Unexpected op indicated: 0x%02X", p->op_code); + err = -EINVAL; + } + return err; +} + +static int decode_dfu(void) +{ + struct dfu_notify_packet *p = dfu_notify_packet_data; + char buf[256]; + + if (notify_length < 3) { + LOG_ERR("Notification too short: %u < 3", notify_length); + return -EINVAL; + } + if (p->magic != 0x60) { + LOG_ERR("First byte not 0x60: 0x%02X", p->magic); + return -EINVAL; + } + switch (p->op) { + case NRF_DFU_OP_OBJECT_CREATE: + snprintf(buf, sizeof(buf), "DFU Create response: 0x%02X", + p->result); + break; + case NRF_DFU_OP_RECEIPT_NOTIF_SET: + snprintf(buf, sizeof(buf), + "DFU Set Receipt Notification response: 0x%02X", + p->result); + break; + case NRF_DFU_OP_CRC_GET: + if (notify_length < 7) { + LOG_ERR("Notification too short: %u < 7", + notify_length); + return -EINVAL; + } + snprintf(buf, sizeof(buf), "DFU CRC response: 0x%02X, " + "offset: 0x%08X, crc: 0x%08X", + p->result, p->crc.offset, p->crc.crc); + break; + case NRF_DFU_OP_OBJECT_EXECUTE: + snprintf(buf, sizeof(buf), "DFU Execute response: 0x%02X", + p->result); + break; + case NRF_DFU_OP_OBJECT_SELECT: + snprintf(buf, sizeof(buf), + "DFU Select response: 0x%02X, offset: 0x%08X," + " crc: 0x%08X, max_size: %u", p->result, + p->select.offset, p->select.crc, p->select.max_size); + max_size = p->select.max_size; + break; + case NRF_DFU_OP_HARDWARE_VERSION: + snprintf(buf, sizeof(buf), + "DFU HW Version Get response: 0x%02X, part: 0x%08X," + " variant: 0x%08X, rom_size: 0x%08X, ram_size: 0x%08X," + " rom_page_size: 0x%08X", p->result, p->hw.part, + p->hw.variant, p->hw.rom_size, p->hw.ram_size, + p->hw.rom_page_size); + flash_page_size = p->hw.rom_page_size; + break; + case NRF_DFU_OP_FIRMWARE_VERSION: + snprintf(buf, sizeof(buf), + "DFU FW Version Get response: 0x%02X, type: 0x%02X," + " version: 0x%02X, addr: 0x%02X, len: 0x%02X", + p->result, p->fw.type, p->fw.version, p->fw.addr, + p->fw.len); + break; + default: + LOG_ERR("Unknown DFU notification: 0x%02X", p->op); + return -EINVAL; + } + if (p->result == NRF_DFU_RES_CODE_SUCCESS) { + if (verbose) { + LOG_INF("%s", buf); + } else { + LOG_DBG("%s", buf); + } + return 0; + } else if (p->result == NRF_DFU_RES_CODE_EXT_ERROR) { + if (p->ext_err_code == NRF_DFU_EXT_ERROR_FW_VERSION_FAILURE) { + LOG_ERR("Cannot downgrade target firmware version!"); + } else if (p->ext_err_code == NRF_DFU_EXT_ERROR_FW_VERSION_FAILURE) { + LOG_ERR("Incompatible target hardware version!"); + } else if (p->ext_err_code == NRF_DFU_EXT_ERROR_FW_VERSION_FAILURE) { + LOG_ERR("Incompatible target SoftDevice version!"); + } else { + LOG_ERR("Extended DFU error: 0x%02X", p->ext_err_code); + } + return -EFAULT; + } + LOG_WRN("DFU operation failed: %d", p->result); + return -EPROTO; +} + +int peripheral_dfu_init(void) +{ + int err; + + k_work_init_delayable(&fota_job_work, fota_job_next); + + err = download_client_init(&dlc, download_client_callback); + if (err) { + return err; + } + return nrf_cloud_fota_ble_set_handler(fota_ble_callback); +} + +int peripheral_dfu_cleanup(void) +{ + ble_subscribe(ble_norm_addr, DFU_BUTTONLESS_UUID, 0); + ble_conn_mgr_remove_conn(ble_dfu_addr); + ble_conn_mgr_rem_desired(ble_dfu_addr, true); + k_sem_give(&peripheral_dfu_active); + return 0; +} + +int peripheral_dfu_config(const char *addr, int size, const char *version, + uint32_t crc, bool init_pkt, bool use_prtk) +{ + struct ble_device_conn *conn; + uint16_t handle; + int err; + + err = ble_conn_mgr_get_conn_by_addr(addr, &conn); + if (err) { + LOG_ERR("Connection not found for addr %s", addr); + return err; + } + + err = k_sem_take(&peripheral_dfu_active, K_NO_WAIT); + if (err) { + LOG_ERR("Peripheral DFU already active."); + conn->dfu_attempts = MAX_DFU_ATTEMPTS; + conn->dfu_pending = true; + return -EAGAIN; + } + + dfu_conn_ptr = NULL; + + strncpy(ble_norm_addr, addr, sizeof(ble_norm_addr)); + + ble_register_notify_callback(notification_callback); + + err = ble_conn_mgr_get_handle_by_uuid(&handle, DFU_BUTTONLESS_UUID, conn); + if (err) { + err = ble_conn_mgr_get_handle_by_uuid(&handle, DFU_CONTROL_POINT_UUID, conn); + if (err) { + LOG_ERR("Device does not support buttonless DFU, and is not in DFU mode"); + k_sem_give(&peripheral_dfu_active); + return -EINVAL; + } + LOG_INF("Device already in DFU mode"); + strncpy(ble_dfu_addr, addr, sizeof(ble_dfu_addr)); + goto ready; + } + + /* need to switch device to DFU mode first */ + ble_conn_mgr_calc_other_addr(addr, ble_dfu_addr, DEVICE_ADDR_LEN, false); + + err = ble_conn_mgr_get_conn_by_addr(ble_dfu_addr, &conn); + if (err) { + LOG_INF("Adding temporary connection to %s for DFU", + ble_dfu_addr); + err = ble_conn_mgr_add_conn(ble_dfu_addr); + if (err) { + goto failed; + } + LOG_INF("Connection added to ble_conn_mgr"); + } else { + LOG_INF("Connection already exists in ble_conn_mgr"); + } + err = ble_conn_mgr_add_desired(ble_dfu_addr, true); + if (err) { + goto failed; + } + + LOG_INF("Enabling indication"); + err = ble_subscribe(ble_norm_addr, DFU_BUTTONLESS_UUID, + BT_GATT_CCC_INDICATE); + if (err) { + goto failed; + } + + LOG_INF("Switching to DFU mode"); + err = send_switch_to_dfu(ble_norm_addr); + if (err) { + goto failed; + } + int max_loops = 300; + + err = ble_conn_mgr_get_conn_by_addr(ble_dfu_addr, &conn); + if (err) { + goto failed; + } + + /* special dfu flag -- don't send info to cloud about this dfu device */ + conn->hidden = true; + + LOG_INF("Waiting for device discovery..."); + while (!conn->discovered && !conn->encode_discovered) { + k_sleep(K_MSEC(100)); + if (!max_loops--) { + LOG_ERR("Timeout: conn:%u, ding:%u, disc:%u, enc:%u, np:%u", + conn->connected, conn->discovering, + conn->discovered, conn->encode_discovered, + conn->num_pairs); + err = -ETIMEDOUT; + goto failed; + } + } + LOG_INF("Continuing BLE DFU"); + +ready: + strncpy(app_version, version, sizeof(app_version)); + image_size = size; + original_crc = crc; + init_packet = init_pkt; + use_printk = use_prtk; + dfu_conn_ptr = conn; + return 0; + +failed: + LOG_ERR("Error configuring update:%d", err); + return err; +} + +int peripheral_dfu_start(const char *host, const char *file, int sec_tag, + const char *apn, size_t fragment_size) +{ + int err = -1; + const int sec_tag_list[1] = { + sec_tag + }; + struct download_client_cfg config = { + .sec_tag_list = sec_tag_list, + .sec_tag_count = ARRAY_SIZE(sec_tag_list), + .pdn_id = 0, + .frag_size_override = fragment_size, + .set_tls_hostname = (sec_tag != -1), + }; + + if (host == NULL || file == NULL) { + peripheral_dfu_cleanup(); + return -EINVAL; + } + + socket_retries_left = CONFIG_FOTA_SOCKET_RETRIES; + + first_fragment = true; + + err = download_client_set_host(&dlc, host, &config); + if (err != 0) { + peripheral_dfu_cleanup(); + return err; + } + + err = download_client_start(&dlc, file, 0); + if (err != 0) { + download_client_disconnect(&dlc); + peripheral_dfu_cleanup(); + return err; + } + + return 0; +} + +static int download_client_callback(const struct download_client_evt *event) +{ + int err = 0; + + if (event == NULL) { + return -EINVAL; + } + + switch (event->id) { + case DOWNLOAD_CLIENT_EVT_FRAGMENT: + if (first_fragment) { + err = download_client_file_size_get(&dlc, &image_size); + if (err) { + LOG_ERR("Error determining file size: %d", err); + } else { + LOG_INF("Downloading %zd bytes", image_size); + } + } + err = peripheral_dfu(event->fragment.buf, + event->fragment.len); + if (err) { + LOG_ERR("Error from peripheral_dfu: %d", err); + } + break; + case DOWNLOAD_CLIENT_EVT_DONE: + LOG_INF("Download client done"); + err = download_client_disconnect(&dlc); + if (err) { + LOG_ERR("Error disconnecting from download client: %d", + err); + } + k_work_reschedule(&fota_job_work, K_SECONDS(1)); + break; + case DOWNLOAD_CLIENT_EVT_ERROR: { + /* In case of socket errors we can return 0 to retry/continue, + * or non-zero to stop + */ + if ((socket_retries_left) && ((event->error == -ENOTCONN) || + (event->error == -ECONNRESET))) { + LOG_WRN("Download socket error. %d retries left...", + socket_retries_left); + socket_retries_left--; + /* Fall through and return 0 below to tell + * download_client to retry + */ + } else { + err = download_client_disconnect(&dlc); + if (err) { + LOG_ERR("Error disconnecting from " + "download client: %d", err); + } + LOG_ERR("Download client error"); + cancel_dfu(NRF_CLOUD_FOTA_ERROR_DOWNLOAD); + err = -EIO; + } + break; + } + default: + break; + } + + return err; +} + +static int start_ble_job(struct nrf_cloud_fota_ble_job *const ble_job) +{ + __ASSERT_NO_MSG(ble_job != NULL); + int ret; + enum nrf_cloud_fota_status status; + + ret = peripheral_dfu_start(ble_job->info.host, ble_job->info.path, + CONFIG_NRF_CLOUD_SEC_TAG, NULL, + CONFIG_NRF_CLOUD_FOTA_DOWNLOAD_FRAGMENT_SIZE); + if (ret) { + LOG_ERR("Failed to start FOTA download: %d", ret); + status = NRF_CLOUD_FOTA_FAILED; + } else { + LOG_INF("Downloading update"); + status = NRF_CLOUD_FOTA_IN_PROGRESS; + if (total_completed_size) { + /* not the first file, so no progress needed here */ + return ret; + } + } + (void)nrf_cloud_fota_ble_job_update(ble_job, status); + + return ret; +} + +static void fota_ble_callback(const struct nrf_cloud_fota_ble_job * + const ble_job) +{ + char addr[BT_ADDR_LE_STR_LEN]; + bool init_pkt; + char *ver = "n/a"; + uint32_t crc = 0; + int sec_tag = CONFIG_NRF_CLOUD_SEC_TAG; + char *apn = NULL; + size_t frag = CONFIG_NRF_CLOUD_FOTA_DOWNLOAD_FRAGMENT_SIZE; + int err; + + bt_addr_to_str(&ble_job->ble_id, addr, sizeof(addr)); + + if (!ble_conn_mgr_is_addr_connected(addr)) { + /* TODO: add ability to queue? */ + LOG_WRN("Device not connected; ignoring job"); + return; + } + + init_pkt = strstr(ble_job->info.path, "dat") != NULL; + + err = peripheral_dfu_config(addr, ble_job->info.file_size, ver, crc, + init_pkt, false); + if (err == -EAGAIN) { + /* already busy; ask for the job when done with current */ + return; + } else if (err) { + /* could not configure, so don't start job */ + cancel_dfu(NRF_CLOUD_FOTA_ERROR_APPLY_FAIL); + return; + } + + memset(fota_files, 0, sizeof(fota_files)); + num_fota_files = 0; + + int i; + char *end; + char *path = ble_job->info.path; + bool done = false; + + /* separate out various file paths to download */ + for (i = 0; i < MAX_FOTA_FILES; i++) { + end = strchr(path, ' '); + if (!end) { + end = &path[strlen(path)]; + done = true; + } + fota_files[i].path = k_calloc(1 + end - path, 1); + if (!fota_files[i].path) { + LOG_ERR("Out of memory"); + return; + } + memcpy(fota_files[i].path, path, end - path); + path = end + 1; + num_fota_files++; + if (done) { + break; + } + } + + /* fix up order of files based on type */ + for (i = 0; i < MAX_FOTA_FILES; i++) { + if (strstr(fota_files[i].path, ".dat")) { + if (i) { /* .dat is not first; make it so */ + char *tmp = fota_files[0].path; + + fota_files[0].path = fota_files[i].path; + fota_files[i].path = tmp; + } + break; + } + } + + active_num = 0; + total_completed_size = 0; + fota_ble_job.info.path = fota_files[active_num].path; + + /* job structure will disappear after this callback, so make a copy */ + fota_ble_job.ble_id = ble_job->ble_id; + fota_ble_job.info.type = ble_job->info.type; + fota_ble_job.info.id = strdup(ble_job->info.id); + fota_ble_job.info.host = strdup(ble_job->info.host); + /* total size of all files */ + fota_ble_job.info.file_size = ble_job->info.file_size; + fota_ble_job.error = NRF_CLOUD_FOTA_ERROR_NONE; + + LOG_INF("Starting BLE DFU to addr:%s, from host:%s, size:%d, " + "init:%d, ver:%s, crc:%u, sec_tag:%d, apn:%s, frag_size:%zd", + addr, fota_ble_job.info.host, + fota_ble_job.info.file_size, + init_packet, ver, crc, sec_tag, + apn ? apn : "", frag); + LOG_INF("Num files:%d", num_fota_files); + for (i = 0; i < MAX_FOTA_FILES; i++) { + if (!fota_files[i].path) { + break; + } + LOG_INF("File:%d path:%s", i + 1, fota_files[i].path); + } + + (void)start_ble_job(&fota_ble_job); +} + +static void fota_job_next(struct k_work *work) +{ + if (!start_next_job()) { + ble_conn_mgr_force_dfu_rediscover(ble_norm_addr); + if (strcmp(ble_norm_addr, ble_dfu_addr) != 0) { + ble_conn_mgr_force_dfu_rediscover(ble_dfu_addr); + } + ble_register_notify_callback(NULL); + LOGPKINF("DFU complete"); + free_job(); + peripheral_dfu_cleanup(); + ble_conn_mgr_check_pending(); + } +} + +static bool start_next_job(void) +{ + /* select next job */ + active_num++; + if (active_num >= num_fota_files) { + LOG_INF("No more files. Done."); + return false; + } + fota_ble_job.info.path = fota_files[active_num].path; + fota_ble_job.error = NRF_CLOUD_FOTA_ERROR_NONE; + + if (!ble_conn_mgr_is_addr_connected(ble_dfu_addr)) { + /* TODO: add ability to queue? */ + LOG_ERR("Device not connected; cancelling remainder of job"); + cancel_dfu(NRF_CLOUD_FOTA_ERROR_APPLY_FAIL); + return true; + } + + /* update globals */ + init_packet = strstr(fota_ble_job.info.path, ".dat") != NULL; + + LOG_INF("Starting second BLE DFU to addr:%s, from host:%s, " + "path:%s, size:%d, init:%d", + ble_dfu_addr, fota_ble_job.info.host, + fota_ble_job.info.path, fota_ble_job.info.file_size, + init_packet); + + int err = start_ble_job(&fota_ble_job); + + return (err == 0); +} + +static void free_job(void) +{ + if (fota_ble_job.info.id) { + free(fota_ble_job.info.id); + fota_ble_job.info.id = NULL; + if (fota_ble_job.info.host) { + free(fota_ble_job.info.host); + fota_ble_job.info.host = NULL; + } + memset(&fota_ble_job, 0, sizeof(fota_ble_job)); + } + + for (int i = 0; i < MAX_FOTA_FILES; i++) { + if (!fota_files[i].path) { + break; + } + k_free(fota_files[i].path); + fota_files[i].path = NULL; + fota_files[i].file_size = 0; + } +} + +static void cancel_dfu(enum nrf_cloud_fota_error error) +{ + ble_register_notify_callback(NULL); + if (fota_ble_job.info.id) { + enum nrf_cloud_fota_status status; + int err; + + if (ble_conn_mgr_is_addr_connected(ble_dfu_addr)) { + LOG_INF("Sending DFU Abort..."); + send_abort(ble_dfu_addr); + } + + /* TODO: adjust error according to actual failure */ + fota_ble_job.error = error; + status = NRF_CLOUD_FOTA_FAILED; + + err = nrf_cloud_fota_ble_job_update(&fota_ble_job, status); + if (err) { + LOG_ERR("Error updating job: %d", err); + } + free_job(); + LOGPKINF("Update cancelled."); + } + peripheral_dfu_cleanup(); + ble_conn_mgr_check_pending(); +} + +static uint8_t peripheral_dfu(const char *buf, size_t len) +{ + static size_t prev_percent; + static size_t prev_update_percent; + static uint32_t prev_crc; + size_t percent = 0; + int err = 0; + + if (first_fragment) { + LOG_INF("Len:%zd, first:%u, size:%d, init:%u", len, + first_fragment, image_size, init_packet); + } + download_size = image_size; + + if (test_mode) { + size_t percent = 0; + + if (first_fragment) { + completed_size = 0; + first_fragment = false; + } + completed_size += len; + + if (download_size) { + percent = (100 * completed_size) / download_size; + } + + LOG_INF("Progress: %zd%%; %zd of %zd", + percent, completed_size, download_size); + + if (percent >= 100) { + LOG_INF("DFU complete"); + } + return 0; + } + + if (first_fragment) { + first_fragment = false; + LOGPKINF("BLE DFU starting to %s...", ble_dfu_addr); + + if (fota_ble_job.info.id) { + fota_ble_job.dl_progress = (100 * total_completed_size) / + fota_ble_job.info.file_size; + err = nrf_cloud_fota_ble_job_update(&fota_ble_job, + NRF_CLOUD_FOTA_DOWNLOADING); + if (err) { + LOG_ERR("Error updating job: %d", err); + free_job(); + peripheral_dfu_cleanup(); + return err; + } + } + prev_percent = 0; + prev_update_percent = 0; + completed_size = 0; + prev_crc = 0; + page_remaining = 0; + finish_page = false; + + if (init_packet) { + verbose = true; + flash_page_size = 0; + + LOG_INF("Loading Init Packet and " + "turning on notifications..."); + err = ble_subscribe(ble_dfu_addr, DFU_CONTROL_POINT_UUID, + BT_GATT_CCC_NOTIFY); + if (err) { + goto cleanup; + } + + LOG_INF("Setting DFU PRN to 0..."); + err = send_prn(ble_dfu_addr, 0); + if (err) { + goto cleanup; + } + + LOG_INF("Querying hardware version..."); + (void)send_hw_version_get(ble_dfu_addr); + + LOG_INF("Querying firmware version (APP)..."); + (void)send_fw_version_get(ble_dfu_addr, + NRF_DFU_FIRMWARE_TYPE_APPLICATION); + + LOG_INF("Sending DFU select command..."); + err = send_select_command(ble_dfu_addr); + if (err) { + goto cleanup; + } + /* TODO: check offset and CRC; do no transfer + * and skip execute if offset != init length or + * CRC mismatch -- however, to do this, we need the + * cloud to send us the expected CRC for the file + */ + if (dfu_notify_packet_data->select.offset != image_size) { + LOG_INF("Image size mismatched, so continue; " + "offset:%u, size:%u", + dfu_notify_packet_data->select.offset, + image_size); + } else { + LOG_INF("Image size matches device!"); + if (dfu_notify_packet_data->select.crc != original_crc) { + LOG_INF("CRC mismatched, so continue; " + "dev crc:0x%08X, this crc:" + "0x%08X", + dfu_notify_packet_data->select.crc, + original_crc); + } else { + LOG_INF("CRC matches device!"); + } + } + + LOG_INF("Sending DFU create command..."); + err = send_create_command(ble_dfu_addr, image_size); + /* This will need to be the size of the entire init + * file. If http chunks it smaller this won't work + */ + if (err) { + goto cleanup; + } + } else { + verbose = false; + finish_page = false; + + LOG_INF("Loading Firmware"); + LOG_INF("Setting DFU PRN to 0..."); + err = send_prn(ble_dfu_addr, 0); + if (err) { + goto cleanup; + } + + LOG_INF("Sending DFU select data..."); + err = send_select_data(ble_dfu_addr); + if (err) { + goto cleanup; + } + if (dfu_notify_packet_data->select.offset != image_size) { + LOG_INF("Image size mismatched, so continue; " + "offset:%u, size:%u", + dfu_notify_packet_data->select.offset, + image_size); + } else { + LOG_INF("Image size matches device!"); + if (dfu_notify_packet_data->select.crc != original_crc) { + LOG_INF("CRC mismatched, so continue; " + "dev crc:0x%08X, this crc:" + "0x%08X", + dfu_notify_packet_data->select.crc, + original_crc); + } else { + LOG_INF("CRC matches device!"); + /* TODO: we could skip the entire file, + * and just send an indication to the + * cloud that the job is complete + */ + } + } + } + } + + /* output data while handing page boundaries */ + while (len) { + if (!finish_page) { + uint32_t file_remaining = download_size - completed_size; + + page_remaining = MIN(max_size, file_remaining); + + LOG_DBG("Page remaining %u, max_size %u, len %u", + page_remaining, max_size, len); + if (!init_packet) { + err = send_create_data(ble_dfu_addr, page_remaining); + if (err) { + goto cleanup; + } + } + if (len < page_remaining) { + LOG_DBG("Need to transfer %u in page", + page_remaining); + finish_page = true; + } else { + if (!completed_size) { + LOG_DBG("No need to finish first page " + "next time"); + } + } + } + + size_t page_len = MIN(len, page_remaining); + + LOG_DBG("Sending DFU data len %u...", page_len); + err = send_data(ble_dfu_addr, buf, page_len); + if (err) { + goto cleanup; + } + + prev_crc = crc32_ieee_update(prev_crc, buf, page_len); + + completed_size += page_len; + total_completed_size += page_len; + len -= page_len; + buf += page_len; + LOG_DBG("Completed size: %u", completed_size); + + page_remaining -= page_len; + if ((page_remaining == 0) || + (completed_size == download_size)) { + finish_page = false; + LOG_DBG("Page is complete"); + } else { + LOG_DBG("Page remaining: %u bytes", page_remaining); + } + + if (!finish_page) { + /* give hardware time to finish; omitting this step + * results in an error on the execute request and/or + * CRC mismatch; sometimes we are too fast so we + * need to retry, because the transfer was still + * going on + */ + for (int i = 1; i <= 5; i++) { + k_sleep(K_MSEC(100)); + + LOG_DBG("Sending DFU request CRC %d...", i); + err = send_request_crc(ble_dfu_addr); + if (err) { + break; + } + if (dfu_notify_packet_data->crc.offset != + completed_size) { + err = -EIO; + } else if (dfu_notify_packet_data->crc.crc != + prev_crc) { + err = -EBADMSG; + } else { + LOG_INF("CRC and length match."); + err = 0; + break; + } + } + if (err == -EIO) { + LOG_ERR("Transfer offset wrong; received: %u," + " expected: %u", + dfu_notify_packet_data->crc.offset, + completed_size); + } + if (err == -EBADMSG) { + LOG_ERR("CRC wrong; received: 0x%08X, " + "expected: 0x%08X", + dfu_notify_packet_data->crc.crc, + prev_crc); + } + if (err) { + goto cleanup; + } + + LOG_DBG("Sending DFU execute..."); + err = send_execute(ble_dfu_addr); + if (err) { + goto cleanup; + } + } + } + + if (download_size) { + percent = (100 * total_completed_size) / fota_ble_job.info.file_size; + } + + if (percent != prev_percent) { + LOGPKINF("Progress: %zd%%, %zd bytes of %zd", percent, + completed_size, download_size); + prev_percent = percent; + + if (fota_ble_job.info.id && + (((percent - prev_update_percent) >= + PROGRESS_UPDATE_INTERVAL) || + (percent >= 100))) { + enum nrf_cloud_fota_status status; + + LOG_INF("Sending job update at %zd%% percent", + percent); + fota_ble_job.error = NRF_CLOUD_FOTA_ERROR_NONE; + if (percent < 100) { + status = NRF_CLOUD_FOTA_DOWNLOADING; + fota_ble_job.dl_progress = percent; + } else { + status = NRF_CLOUD_FOTA_SUCCEEDED; + fota_ble_job.dl_progress = 100; + if (dfu_conn_ptr) { + dfu_conn_ptr->dfu_pending = false; + } + } + err = nrf_cloud_fota_ble_job_update(&fota_ble_job, + status); + if (err) { + LOG_ERR("Error updating job: %d", err); + goto cleanup; + } + prev_update_percent = percent; + } + } + + return err; + +cleanup: + cancel_dfu(NRF_CLOUD_FOTA_ERROR_APPLY_FAIL); + return err; +} + +static int send_data(char *ble_addr, const char *buf, size_t len) +{ + /* "chunk" The data */ + int idx = 0; + int err = 0; + + while (idx < len) { + uint8_t size = MIN(MAX_CHUNK_SIZE, (len - idx)); + + LOG_DBG("Sending write without response: %d, %d", size, idx); + err = gatt_write_without_response(ble_addr, DFU_PACKET_UUID, + (uint8_t *)&buf[idx], size); + if (err) { + LOG_ERR("Error writing chunk at %d size %u: %d", + idx, size, err); + break; + } + idx += size; + } + return err; +} + +/* name should be a string constant so log_strdup not needed */ +static int do_cmd(char *ble_addr, bool normal_mode, uint8_t *buf, uint16_t len, + char *name, bool verbose) +{ + int err; + const char *uuid; + + if (normal_mode) { + uuid = DFU_BUTTONLESS_UUID; + } else { + uuid = DFU_CONTROL_POINT_UUID; + } + notify_received = false; + + err = gatt_write(ble_addr, uuid, buf, len, on_sent); + if (err) { + LOG_ERR("Error writing %s: %d", name, err); + return err; + } + err = wait_for_notification(); + if (err) { + LOG_ERR("Timeout waiting for notification from %s: %d", + name, err); + return err; + } + err = normal_mode ? decode_secure_dfu() : decode_dfu(); + if (err && verbose) { + LOG_ERR("Notification decode error from %s: %d", + name, err); + } + return err; +} + +static int send_switch_to_dfu(char *ble_addr) +{ + char smol_buf[1]; + + smol_buf[0] = DFU_OP_ENTER_BOOTLOADER; + + return do_cmd(ble_addr, true, smol_buf, sizeof(smol_buf), "DFU", true); +} + + +/* set number of writes between CRC reports; 0 disables */ +static int send_prn(char *ble_addr, uint16_t receipt_rate) +{ + char smol_buf[3]; + + smol_buf[0] = NRF_DFU_OP_RECEIPT_NOTIF_SET; + smol_buf[1] = receipt_rate & 0xff; + smol_buf[2] = (receipt_rate >> 8) & 0xff; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "PRN", true); +} + +static int send_select_command(char *ble_addr) +{ + char smol_buf[2]; + + smol_buf[0] = NRF_DFU_OP_OBJECT_SELECT; + smol_buf[1] = 0x01; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Select Cmd", true); +} + +static int send_select_data(char *ble_addr) +{ + char smol_buf[2]; + + smol_buf[0] = NRF_DFU_OP_OBJECT_SELECT; + smol_buf[1] = 0x02; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Select Data", true); +} + +static int send_create_command(char *ble_addr, uint32_t size) +{ + uint8_t smol_buf[6]; + + smol_buf[0] = NRF_DFU_OP_OBJECT_CREATE; + smol_buf[1] = 0x01; + smol_buf[2] = size & 0xFF; + smol_buf[3] = (size >> 8) & 0xFF; + smol_buf[4] = (size >> 16) & 0xFF; + smol_buf[5] = (size >> 24) & 0xFF; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Create Cmd", true); +} + +static int send_create_data(char *ble_addr, uint32_t size) +{ + uint8_t smol_buf[6]; + + LOG_DBG("Size is %d", size); + + smol_buf[0] = NRF_DFU_OP_OBJECT_CREATE; + smol_buf[1] = 0x02; + smol_buf[2] = size & 0xFF; + smol_buf[3] = (size >> 8) & 0xFF; + smol_buf[4] = (size >> 16) & 0xFF; + smol_buf[5] = (size >> 24) & 0xFF; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Create Data", true); +} + +static void on_sent(struct bt_conn *conn, uint8_t err, + struct bt_gatt_write_params *params) +{ + const void *data; + uint16_t length; + + /* TODO: Make a copy of volatile data that is passed to the + * callback. Check err value at least in the wait function. + */ + data = params->data; + length = params->length; + + LOG_DBG("Resp err:0x%02X, from write:", err); + LOG_HEXDUMP_DBG(data, length, "sent"); +} + +static int send_request_crc(char *ble_addr) +{ + char smol_buf[1]; + + /* Requesting 32 bit offset followed by 32 bit CRC */ + smol_buf[0] = NRF_DFU_OP_CRC_GET; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "CRC Request", true); +} + +static int send_execute(char *ble_addr) +{ + char smol_buf[1]; + + LOG_DBG("Sending Execute"); + + smol_buf[0] = NRF_DFU_OP_OBJECT_EXECUTE; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Execute", true); +} + +static int send_hw_version_get(char *ble_addr) +{ + char smol_buf[1]; + + LOG_INF("Sending HW Version Get"); + + smol_buf[0] = NRF_DFU_OP_HARDWARE_VERSION; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Get HW Ver", false); +} + +static int send_fw_version_get(char *ble_addr, uint8_t fw_type) +{ + char smol_buf[2]; + + LOG_INF("Sending FW Version Get"); + + smol_buf[0] = NRF_DFU_OP_FIRMWARE_VERSION; + smol_buf[1] = fw_type; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Get FW Ver", false); +} + +static int send_abort(char *ble_addr) +{ + char smol_buf[1]; + + smol_buf[0] = 0x0C; + + return do_cmd(ble_addr, false, smol_buf, + sizeof(smol_buf), "Abort", true); +} diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/dfu/peripheral_dfu.h b/samples/cellular/nrf_cloud_multi_service/src/ble/dfu/peripheral_dfu.h new file mode 100644 index 000000000000..76b982ec13a4 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/dfu/peripheral_dfu.h @@ -0,0 +1,16 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef PERIPHERAL_DFU_H_ +#define PERIPHERAL_DFU_H_ + +int peripheral_dfu_init(void); +int peripheral_dfu_config(const char *addr, int size, const char *version, + uint32_t crc, bool init_pkt, bool use_printk); +int peripheral_dfu_start(const char *host, const char *file, int sec_tag, + const char *apn, size_t fragment_size); +int peripheral_dfu_cleanup(void); + +#endif diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/flash/CMakeLists.txt b/samples/cellular/nrf_cloud_multi_service/src/ble/flash/CMakeLists.txt new file mode 100644 index 000000000000..4f724759867d --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/flash/CMakeLists.txt @@ -0,0 +1,11 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_include_directories(include) +target_sources_ifdef( + CONFIG_FLASH_TEST + app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/flash_test.c + ) diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/flash/flash_test.c b/samples/cellular/nrf_cloud_multi_service/src/ble/flash/flash_test.c new file mode 100644 index 000000000000..d26575e275fa --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/flash/flash_test.c @@ -0,0 +1,139 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include + +#if CONFIG_SPI_NOR || DT_NODE_HAS_STATUS(DT_INST(0, jedec_spi_nor), okay) +#define FLASH_NAME "JEDEC SPI-NOR" +#elif CONFIG_NORDIC_QSPI_NOR || DT_NODE_HAS_STATUS(DT_INST(0, nordic_qspi_nor), okay) +#define FLASH_NAME "JEDEC QSPI-NOR" +#else +#error Unsupported flash driver +#endif + +#define FLASH_TEST_REGION_OFFSET 0xff000 +#define FLASH_SECTOR_SIZE 4096 + +#define MEMCTRL_NODE DT_NODELABEL(ext_mem_ctrl) + +#define MEMCTRL_GPIO_CTRL DT_GPIO_CTLR(MEMCTRL_NODE, gpios) +#define MEMCTRL_GPIO_PIN DT_GPIO_PIN(MEMCTRL_NODE, gpios) + +/** + * Set the external mem control pin to high to + * enable access to the external memory chip. + */ +static int flash_test_init(void) +{ + const struct device *port; + int err; + + printk("%s\n", __func__); + + port = DEVICE_DT_GET(MEMCTRL_GPIO_CTRL); + if (!port) { + printk("memctrl port not found: %d\n", errno); + return -EIO; + } + + err = gpio_pin_configure(port, MEMCTRL_GPIO_PIN, GPIO_OUTPUT_HIGH); + if (err) { + printk("error on gpio_pin_configure(): %d\n", err); + return err; + } + + printk("init complete\n"); + return err; +} + +SYS_INIT(flash_test_init, POST_KERNEL, CONFIG_SPI_NOR_INIT_PRIORITY); + +int flash_test(void) +{ + const uint8_t expected[] = { + 0x55, 0xaa, 0x66, 0x99, 0xa5, 0x5a, 0x69, 0x96, + 0xa5, 0x5a, 0x96, 0x69, 0x5a, 0xa5, 0x4b, 0xb4 + }; + const size_t len = sizeof(expected); + uint8_t buf[sizeof(expected)]; + const struct device *flash_dev; + int rc; + int ret = 0; + + printk("\n" FLASH_NAME " SPI flash testing\n"); + printk("==========================\n"); + +#if CONFIG_SPI_NOR || DT_NODE_HAS_STATUS(DT_INST(0, jedec_spi_nor), okay) + flash_dev = DEVICE_DT_GET_ONE(jedec_spi_nor); +#elif CONFIG_NORDIC_QSPI_NOR || DT_NODE_HAS_STATUS(DT_INST(0, nordic_qspi_nor), okay) + flash_dev = DEVICE_DT_GET_ONE(jedec_qspi_nor); +#endif + + if (!flash_dev) { + printk("SPI flash driver %s was not found!\n", FLASH_NAME); + ret = -EIO; + goto error; + } + + printk("\nTest 1: Flash erase: "); + + rc = flash_erase(flash_dev, FLASH_TEST_REGION_OFFSET, + FLASH_SECTOR_SIZE); + if (rc != 0) { + printk("FAIL - %d\n", rc); + } else { + printk("PASS\n"); + } + + printk("\nTest 2: Flash write %u bytes: ", len); + + rc = flash_write(flash_dev, FLASH_TEST_REGION_OFFSET, expected, len); + if (rc != 0) { + printk("FAIL - %d\n", rc); + ret = -EIO; + goto error; + } else { + printk("PASS\n"); + } + + printk("\nTest 3: Flash read %u bytes: ", len); + memset(buf, 0, len); + rc = flash_read(flash_dev, FLASH_TEST_REGION_OFFSET, buf, len); + if (rc != 0) { + printk("FAIL - %d\n", rc); + ret = -EIO; + goto error; + } else { + printk("PASS\n"); + } + + printk("\nTest 4: Compare %u bytes: ", len); + if (memcmp(expected, buf, len) != 0) { + const uint8_t *wp = expected; + const uint8_t *rp = buf; + const uint8_t *rpe = rp + len; + + ret = -EFAULT; + printk("FAIL - %d\n", ret); + while (rp < rpe) { + printk("%08x wrote %02x read %02x %s\n", + (uint32_t)(FLASH_TEST_REGION_OFFSET + (rp - buf)), + *wp, *rp, (*rp == *wp) ? "match" : "MISMATCH"); + ++rp; + ++wp; + } + goto error; + } else { + printk("PASS\n"); + } + return 0; + +error: + return ret; +} diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/flash/include/flash_test.h b/samples/cellular/nrf_cloud_multi_service/src/ble/flash/include/flash_test.h new file mode 100644 index 000000000000..bdfb13ad5376 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/flash/include/flash_test.h @@ -0,0 +1,10 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _FLASH_TEST_H_ + +int flash_test(void); + +#endif /* _FLASH_TEST_H_ */ diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/gateway.c b/samples/cellular/nrf_cloud_multi_service/src/ble/gateway.c new file mode 100644 index 000000000000..c945f63ef05f --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/gateway.c @@ -0,0 +1,694 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(CONFIG_NRF_MODEM_LIB) +#include +#endif +#include +#include + +#include "gateway.h" +#include "nrf_cloud_mem.h" + +#include "cJSON.h" +#include "cJSON_os.h" +#include "ble.h" +#include "ble_codec.h" +#include "ble_conn_mgr.h" + +LOG_MODULE_REGISTER(gateway, CONFIG_NRFCLOUD_BLE_GATEWAY_LOG_LEVEL); + +#define AT_CMNG_READ_LEN 97 + +#define CLOUD_PROC_STACK_SIZE 2048 +#define CLOUD_PROC_PRIORITY 5 + +#define GET_PSK_ID "AT%CMNG=2,16842753,4" +#define GET_PSK_ID_LEN (sizeof(GET_PSK_ID)-1) +#define GET_PSK_ID_ERR "ERROR" + +/* Uncomment below to enable writing characteristics from cloud_data_process() + * rather than from gateway_handler(). However, writing added too much data + * to the fifo. There was no memory to unsubscribe from memory intensive + * subscribes. + */ +#define QUEUE_CHAR_WRITES +#define QUEUE_CHAR_READS + +#define VALUE_BUF_SIZE 256 +static uint8_t value_buf[VALUE_BUF_SIZE]; + +char gateway_id[NRF_CLOUD_CLIENT_ID_LEN+1]; + +struct cloud_data_t { + void *fifo_reserved; + char addr[BT_ADDR_STR_LEN]; + char uuid[UUID_STR_LEN]; + uint8_t client_char_config; + bool read; + bool ccc; + bool sub; +#if defined(QUEUE_CHAR_WRITES) + bool write; + size_t data_len; + uint8_t data[VALUE_BUF_SIZE]; +#endif +}; + +static atomic_t queued_cloud_data; +K_FIFO_DEFINE(cloud_data_fifo); + +static int gateway_handler(cJSON *json); + +int gateway_shadow_delta_handler(const struct nrf_cloud_obj_shadow_delta *delta) +{ + cJSON *root_obj = delta->state.json; + cJSON *desired_connections_obj = cJSON_GetObjectItem(root_obj, "desiredConnections"); + + if (!desired_connections_obj) { + LOG_DBG("No desired connections provided"); + return 0; + } + + return desired_conns_handler(desired_connections_obj); +} + +int gateway_shadow_accepted_handler(const struct nrf_cloud_obj *desired) +{ + ARG_UNUSED(desired); + + int err = nrf_cloud_shadow_transform_request("state.desired.desiredConnections", 1024); + + if (err) { + LOG_ERR("Error requesting shadow tf: %d", err); + } + return err; +} + +int gateway_shadow_transform_handler(const struct nrf_cloud_obj *desired_conns) +{ + if (!desired_conns || (desired_conns->type != NRF_CLOUD_OBJ_TYPE_JSON) || + !(desired_conns->json)) { + return -EINVAL; + } + cJSON *desired_tf = desired_conns->json; + cJSON *desired = cJSON_GetObjectItem(desired_tf, "tf"); + + return desired_conns_handler(desired); +} + +static void cloud_data_process(int unused1, int unused2, int unused3) +{ + ARG_UNUSED(unused1); + ARG_UNUSED(unused2); + ARG_UNUSED(unused3); + struct k_mutex lock; + int ret; + + k_mutex_init(&lock); + + while (1) { + LOG_DBG("Waiting for cloud_data_fifo"); + struct cloud_data_t *cloud_data = k_fifo_get(&cloud_data_fifo, K_FOREVER); + + atomic_dec(&queued_cloud_data); + LOG_DBG("Dequeued cloud_data_fifo element; still queued: %ld", + atomic_get(&queued_cloud_data)); + + if (cloud_data != NULL) { + k_mutex_lock(&lock, K_FOREVER); + + if (cloud_data->sub) { + LOG_DBG("Dequeued sub msg"); + ble_subscribe(cloud_data->addr, + cloud_data->uuid, + cloud_data->client_char_config); + } +#if defined(QUEUE_CHAR_READS) + else if (cloud_data->read) { + LOG_DBG("Dequeued gatt_read request %s, %s, %u", + cloud_data->addr, + cloud_data->uuid, + cloud_data->ccc); + ret = gatt_read(cloud_data->addr, + cloud_data->uuid, + cloud_data->ccc); + if (ret) { + LOG_ERR("Error on gatt_read(%s, %s, %u): %d", + cloud_data->addr, + cloud_data->uuid, + cloud_data->ccc, + ret); + } + } +#endif +#if defined(QUEUE_CHAR_WRITES) + else if (cloud_data->write) { + LOG_DBG("Dequeued gatt write request"); + ret = gatt_write(cloud_data->addr, + cloud_data->uuid, + cloud_data->data, + cloud_data->data_len, NULL); + if (ret) { + LOG_ERR("Error on gatt_write(%s, %s): %d", + cloud_data->addr, + cloud_data->uuid, + ret); + } + } +#endif + k_free(cloud_data); + k_mutex_unlock(&lock); + } + } +} + +K_THREAD_DEFINE(cloud_proc_thread, CLOUD_PROC_STACK_SIZE, + cloud_data_process, NULL, NULL, NULL, + K_LOWEST_APPLICATION_THREAD_PRIO, 0, 0); + +static cJSON *json_object_decode(cJSON *obj, const char *str) +{ + return obj ? cJSON_GetObjectItem(obj, str) : NULL; +} + +static bool compare(const char *s1, const char *s2) +{ + return !strncmp(s1, s2, strlen(s2)); +} + +int gateway_data_handler(const struct nrf_cloud_data *const dev_msg) +{ + struct nrf_cloud_obj msg_obj; + int err = nrf_cloud_obj_input_decode(&msg_obj, dev_msg); + + if (err) { + /* The message isn't JSON or otherwise couldn't be parsed. */ + LOG_DBG("A general topic device message of length %d could not be parsed.", + dev_msg->len); + return err; + } + + if (msg_obj.type != NRF_CLOUD_OBJ_TYPE_JSON) { + LOG_DBG("Wrong message type for gateway: %d", msg_obj.type); + err = -EINVAL; + } else { + err = gateway_handler(msg_obj.json); + } + + (void)nrf_cloud_obj_free(&msg_obj); + return err; +} + +static int gateway_handler(cJSON *root_obj) +{ + int ret = 0; + cJSON *desired_obj; + + cJSON *type_obj; + cJSON *operation_obj; + + cJSON *ble_address; + + cJSON *chrc_uuid; + cJSON *service_uuid; + cJSON *desc_arr; + uint8_t desc_buf[2] = {0}; + uint8_t desc_len = 0; + + cJSON *value_arr; + uint8_t value_len = 0; + + if (root_obj == NULL) { + return -ENOENT; + } + + type_obj = json_object_decode(root_obj, "type"); + + if (type_obj == NULL) { + ret = -ENOENT; + goto exit_handler; + } + + const char *type_str = type_obj->valuestring; + + if (!compare(type_str, "operation")) { + ret = -EINVAL; + goto exit_handler; + } + + operation_obj = json_object_decode(root_obj, "operation"); + desired_obj = json_object_decode(operation_obj, "type"); + + if (compare(desired_obj->valuestring, "scan")) { + desired_obj = json_object_decode(operation_obj, "scanType"); + switch (desired_obj->valueint) { + case 0: + /* submit k_work to respond */ + LOG_INF("Cloud requested BLE scan"); + scan_start(false); + break; + case 1: + /* submit k_work to respond */ + /* TODO: Add beacon support */ + break; + default: + break; + } + + } else if (compare(desired_obj->valuestring, + "device_characteristic_value_read")) { + ble_address = json_object_decode(operation_obj, + "deviceAddress"); + chrc_uuid = json_object_decode(operation_obj, + "characteristicUUID"); + + LOG_INF("Got device_characteristic_value_read: %s", + ble_address->valuestring); + if ((ble_address != NULL) && (chrc_uuid != NULL)) { +#if defined(QUEUE_CHAR_READS) + struct cloud_data_t cloud_data = { + .read = true, + .ccc = false, + .sub = false + }; + + memcpy(&cloud_data.addr, + ble_address->valuestring, + strlen(ble_address->valuestring)); + memcpy(&cloud_data.uuid, + chrc_uuid->valuestring, + strlen(chrc_uuid->valuestring)); + + size_t size = sizeof(struct cloud_data_t); + char *mem_ptr = k_malloc(size); + + if (mem_ptr == NULL) { + LOG_ERR("Out of memory!"); + ret = -ENOMEM; + goto exit_handler; + } + + memcpy(mem_ptr, &cloud_data, size); + atomic_inc(&queued_cloud_data); + k_fifo_put(&cloud_data_fifo, mem_ptr); + LOG_DBG("Queued device_characteristic_value_read addr:%s, uuid:%s; " + "queued: %ld", + cloud_data.addr, cloud_data.uuid, atomic_get(&queued_cloud_data)); +#else + ret = gatt_read(ble_address->valuestring, chrc_uuid->valuestring, false); + if (ret) { + LOG_ERR("Error on gatt_read(%s, %s, 0): %d", + ble_address->valuestring, + chrc_uuid->valuestring, + ret); + } +#endif + } + + } else if (compare(desired_obj->valuestring, + "device_descriptor_value_read")) { + + ble_address = json_object_decode(operation_obj, + "deviceAddress"); + chrc_uuid = json_object_decode(operation_obj, + "characteristicUUID"); + + LOG_INF("Got device_descriptor_value_read: %s", + ble_address->valuestring); + if ((ble_address != NULL) && (chrc_uuid != NULL)) { +#if defined(QUEUE_CHAR_READS) + struct cloud_data_t cloud_data = { + .read = true, + .ccc = true, + .sub = false + }; + + memcpy(&cloud_data.addr, + ble_address->valuestring, + strlen(ble_address->valuestring)); + memcpy(&cloud_data.uuid, + chrc_uuid->valuestring, + strlen(chrc_uuid->valuestring)); + + size_t size = sizeof(struct cloud_data_t); + char *mem_ptr = k_malloc(size); + + if (mem_ptr == NULL) { + LOG_ERR("Out of memory!"); + ret = -ENOMEM; + goto exit_handler; + } + + memcpy(mem_ptr, &cloud_data, size); + atomic_inc(&queued_cloud_data); + k_fifo_put(&cloud_data_fifo, mem_ptr); + LOG_DBG("Queued device_descriptor_value_read addr:%s, uuid:%s, queued: %ld", + cloud_data.addr, cloud_data.uuid, atomic_get(&queued_cloud_data)); +#else + ret = gatt_read(ble_address->valuestring, chrc_uuid->valuestring, true); + if (ret) { + LOG_ERR("Error on gatt_read(%s, %s, 1): %d", + ble_address->valuestring, + chrc_uuid->valuestring, + ret); + } +#endif + } + + } else if (compare(desired_obj->valuestring, + "device_descriptor_value_write")) { + ble_address = json_object_decode(operation_obj, + "deviceAddress"); + chrc_uuid = json_object_decode(operation_obj, + "characteristicUUID"); + desc_arr = json_object_decode(operation_obj, + "descriptorValue"); + + desc_len = cJSON_GetArraySize(desc_arr); + for (int i = 0; i < desc_len; i++) { + cJSON *item = cJSON_GetArrayItem(desc_arr, i); + + desc_buf[i] = item->valueint; + } + + LOG_DBG("Got device_descriptor_value_write " + "addr: %s, uuid: %s, value: (0x%02X, 0x%02X)", + ble_address->valuestring, chrc_uuid->valuestring, + desc_buf[0], desc_buf[1]); + + if ((ble_address != NULL) && (chrc_uuid != NULL)) { +#if defined(QUEUE_CHAR_WRITES) + struct cloud_data_t cloud_data = { + .sub = true, + .read = false + }; + + memcpy(&cloud_data.addr, + ble_address->valuestring, + strlen(ble_address->valuestring)); + memcpy(&cloud_data.uuid, + chrc_uuid->valuestring, + strlen(chrc_uuid->valuestring)); + + cloud_data.client_char_config = desc_buf[0]; + + size_t size = sizeof(struct cloud_data_t); + char *mem_ptr = k_malloc(size); + + if (mem_ptr == NULL) { + LOG_ERR("Out of memory!"); + ret = -ENOMEM; + goto exit_handler; + } + + memcpy(mem_ptr, &cloud_data, size); + atomic_inc(&queued_cloud_data); + k_fifo_put(&cloud_data_fifo, mem_ptr); + LOG_DBG("Queued device_descriptor_value_write addr:%s, uuid:%s, " + "ccc:%d, sub:%d, conf:%d, queued:%ld", + cloud_data.addr, + cloud_data.uuid, + cloud_data.ccc, cloud_data.sub, + cloud_data.client_char_config, + atomic_get(&queued_cloud_data)); +#else + ret = gatt_write(ble_address->valuestring, + chrc_uuid->valuestring, + desc_buf, desc_len, NULL); + if (ret) { + LOG_ERR("Error on gatt_write(%s, %s): %d", + ble_address->valuestring, + chrc_uuid->valuestring, + ret); + } +#endif + } + + } else if (compare(desired_obj->valuestring, + "device_characteristic_value_write")) { + ble_address = json_object_decode(operation_obj, + "deviceAddress"); + chrc_uuid = json_object_decode(operation_obj, + "characteristicUUID"); + service_uuid = json_object_decode(operation_obj, + "serviceUUID"); + value_arr = json_object_decode(operation_obj, + "characteristicValue"); + + if (cJSON_IsString(value_arr)) { + char *str = cJSON_GetStringValue(value_arr); + + strncpy(value_buf, str, sizeof(value_buf) - 1); + value_len = strlen(value_buf); + } else { + value_len = MIN(cJSON_GetArraySize(value_arr), VALUE_BUF_SIZE); + + for (int i = 0; i < value_len; i++) { + cJSON *item = cJSON_GetArrayItem(value_arr, i); + + value_buf[i] = item->valueint; + } + } + + LOG_DBG("Got device_characteristic_value_write " + "addr: %s, svc uuid:%s, chrc uuid:%s", + ble_address->valuestring, + service_uuid ? service_uuid->valuestring : "n/a", + chrc_uuid ? chrc_uuid->valuestring : "n/a"); + LOG_HEXDUMP_DBG(value_buf, value_len, "value"); + + if ((ble_address != NULL) && (chrc_uuid != NULL)) { +#if defined(QUEUE_CHAR_WRITES) + struct cloud_data_t cloud_data = { + .write = true, + .data_len = value_len + }; + + memcpy(&cloud_data.addr, + ble_address->valuestring, + strlen(ble_address->valuestring)); + memcpy(&cloud_data.uuid, + chrc_uuid->valuestring, + strlen(chrc_uuid->valuestring)); + memcpy(&cloud_data.data, + value_buf, + value_len); + + size_t size = sizeof(struct cloud_data_t); + char *mem_ptr = k_malloc(size); + + if (mem_ptr == NULL) { + LOG_ERR("Out of memory!"); + ret = -ENOMEM; + goto exit_handler; + } + + memcpy(mem_ptr, &cloud_data, size); + atomic_inc(&queued_cloud_data); + k_fifo_put(&cloud_data_fifo, mem_ptr); + LOG_DBG("Queued device_characteristic_value_write " + "addr:%s, uuid:%s, queued:%ld", + cloud_data.addr, cloud_data.uuid, atomic_get(&queued_cloud_data)); +#else + ret = gatt_write(ble_address->valuestring, + chrc_uuid->valuestring, + value_buf, value_len, NULL); + if (ret) { + LOG_ERR("Error on gatt_write(%s, %s): %d", + ble_address->valuestring, + chrc_uuid->valuestring, + ret); + } +#endif + } + + } else if (compare(desired_obj->valuestring, "device_discover")) { + ble_address = json_object_decode(operation_obj, + "deviceAddress"); + if (ble_address != NULL) { + LOG_INF("Cloud requested device_discover: %s", + ble_address->valuestring); + ble_conn_mgr_rediscover(ble_address->valuestring); + } + } + +exit_handler: + return ret; +} + +void set_log_panic(void) +{ + LOG_PANIC(); +} + +static void starting_button_handler(void) +{ +#if defined(CONFIG_ENTER_52840_MCUBOOT_VIA_BUTTON) + /* the active board file need to include the functions: + * nrf52840_reset_to_mcuboot() and nrf52840_wait_boot_low() + */ + if (ui_button_is_active(1)) { + printk("BOOT BUTTON HELD\n"); + /* ui_led_set_pattern(UI_BLE_BUTTON, PWM_DEV_1); */ + while (ui_button_is_active(1)) { + k_sleep(K_MSEC(500)); + } + printk("BOOT BUTTON RELEASED\n"); + if (!is_boot_selected()) { + printk("Boot held after reset but not long enough" + " to select the nRF52840 bootloader!\n"); + /* ui_led_set_pattern(UI_BLE_OFF, PWM_DEV_1); */ + } else { + /* User wants to update the 52840 */ + nrf52840_reset_to_mcuboot(); + + /* wait forever for signal from other side so we can + * continue + */ + int err; + + err = nrf52840_wait_boot_complete(WAIT_BOOT_TIMEOUT_MS); + if (err == 0) { + /* ui_led_set_pattern(UI_BLE_OFF, PWM_DEV_1); */ + k_sleep(K_SECONDS(1)); + printk("nRF52840 update complete\n"); + } else { + /* unable to monitor; just halt */ + printk("Error waiting for nrf52840 reboot: %d\n", + err); + for (;;) { + k_sleep(K_MSEC(500)); + } + } + } + } +#endif +} + +void init_gateway(void) +{ + int err; + + starting_button_handler(); + err = ble_init(); + if (err) { + LOG_ERR("Error initializing BLE: %d", err); + } else { + /* Set BLE is disconnected LED pattern */ + } + + ble_codec_init(); + +#if CONFIG_GATEWAY_BLE_FOTA + ret = peripheral_dfu_init(); + if (ret) { + LOG_ERR("Error initializing BLE DFU: %d", ret); + } +#endif +} + +void reset_gateway(void) +{ + ble_conn_mgr_init(); + ble_conn_mgr_clear_desired(true); + ble_stop_activity(); +} + +int g2c_send(const struct nrf_cloud_data *output) +{ + struct nrf_cloud_tx_data msg; + + msg.data.ptr = output->ptr; + msg.data.len = output->len; + msg.topic_type = NRF_CLOUD_TOPIC_MESSAGE; + msg.qos = MQTT_QOS_1_AT_LEAST_ONCE; + msg.id = 0; + msg.obj = NULL; + + return nrf_cloud_send(&msg); +} + +int gw_shadow_publish(const struct nrf_cloud_data *output) +{ + struct nrf_cloud_tx_data msg; + + msg.data.ptr = output->ptr; + msg.data.len = output->len; + msg.topic_type = NRF_CLOUD_TOPIC_STATE; + msg.qos = MQTT_QOS_1_AT_LEAST_ONCE; + msg.id = 0; + msg.obj = NULL; + + return nrf_cloud_send(&msg); +} + +void device_shutdown(bool reboot) +{ + int err; + + LOG_PANIC(); + if (!reboot) { + LOG_INF("Shutting down..."); +#if defined(CONFIG_MODEM_WAKEUP_PIN) + nrf_gpio_cfg_input(CONFIG_MODEM_WAKEUP_PIN, + NRF_GPIO_PIN_PULLUP); + nrf_gpio_cfg_sense_set(CONFIG_MODEM_WAKEUP_PIN, + NRF_GPIO_PIN_SENSE_LOW); +#endif + } + + /* Set BLE shutdown pattern */ + + LOG_INF("Disconnect from cloud..."); + err = nrf_cloud_disconnect(); + if (err) { + LOG_ERR("Error closing cloud: %d", + err); + } + + LOG_INF("Power off LTE..."); + err = lte_lc_power_off(); + if (err) { + LOG_ERR("Error powering off LTE: %d", + err); + } + + LOG_INF("Shutdown modem..."); + err = nrf_modem_shutdown(); + if (err) { + LOG_ERR("Error on bsd_shutdown(): %d", + err); + } + + if (reboot) { + LOG_INF("Rebooting..."); + k_sleep(K_SECONDS(1)); + +#if defined(CONFIG_REBOOT) + sys_reboot(SYS_REBOOT_COLD); +#else + LOG_ERR("sys_reboot not defined: " + "enable CONFIG_REBOOT and rebuild"); +#endif + } else { + LOG_INF("Power down."); + k_sleep(K_SECONDS(1)); + NRF_REGULATORS_NS->SYSTEMOFF = 1; + } +} diff --git a/samples/cellular/nrf_cloud_multi_service/src/ble/gateway.h b/samples/cellular/nrf_cloud_multi_service/src/ble/gateway.h new file mode 100644 index 000000000000..31ed9b8e3c04 --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/src/ble/gateway.h @@ -0,0 +1,34 @@ +/* Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef GATEWAY_CLOUD_TRANSPORT__ +#define GATEWAY_CLOUD_TRANSPORT__ + +#include +#define NRF_CLOUD_CLIENT_ID_LEN 128 +extern char gateway_id[NRF_CLOUD_CLIENT_ID_LEN+1]; + +struct cloud_msg; + +void device_shutdown(bool reboot); +void control_cloud_connection(bool enable); +void cli_init(void); +void set_log_panic(void); +void init_gateway(void); +void reset_gateway(void); +int g2c_send(const struct nrf_cloud_data *output); +int gw_shadow_publish(const struct nrf_cloud_data *output); +int gateway_shadow_delta_handler(const struct nrf_cloud_obj_shadow_delta *delta); +int gateway_shadow_accepted_handler(const struct nrf_cloud_obj *desired); +int gateway_shadow_transform_handler(const struct nrf_cloud_obj *desired_conns); +int gateway_data_handler(const struct nrf_cloud_data *const dev_msg); + +#if defined(CONFIG_ENTER_52840_MCUBOOT_VIA_BUTTON) +/* functions defined in the board's .c file */ +int nrf52840_reset_to_mcuboot(void); +int nrf52840_wait_boot_complete(int timeout_ms); +#endif + +#endif diff --git a/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.c b/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.c index 885662550942..066038ab0a51 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.c +++ b/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.c @@ -24,6 +24,9 @@ #include "location_tracking.h" #include "led_control.h" #include "shadow_config.h" +#if defined(CONFIG_NRF_CLOUD_GATEWAY) +#include "gateway.h" +#endif LOG_MODULE_REGISTER(cloud_connection, CONFIG_MULTI_SERVICE_LOG_LEVEL); @@ -40,6 +43,7 @@ static K_EVENT_DEFINE(cloud_events); /* Atomic status flag tracking whether an initial association is in progress. */ static atomic_t initial_association; static bool device_deleted; +static bool connection_allowed = true; static int reconnect_seconds = CONFIG_CLOUD_CONNECTION_RETRY_TIMEOUT_SECONDS; /* Helper functions for pending on pendable events. */ @@ -68,6 +72,11 @@ bool is_device_deleted(void) return device_deleted; } +void control_cloud_connection(bool enable) +{ + connection_allowed = enable; +} + /* Wait for a connection result, and return true if connection was successful within the timeout, * otherwise return false. */ @@ -164,7 +173,7 @@ void register_general_dev_msg_handler(dev_msg_handler_cb_t handler_cb) /* This function causes the cloud to disconnect, and updates internal state accordingly. * - * It is also triggerd by cloud disconnection, to update internal state. + * It is also triggered by cloud disconnection, to update internal state. * * In this latter case, an unnecessary "Disconnecting from nRF Cloud" and * "Already disconnected from nRF Cloud" will be printed. @@ -367,10 +376,6 @@ static void handle_shadow_event(struct nrf_cloud_obj_shadow_data *const shadow) int err; if ((shadow->type == NRF_CLOUD_OBJ_SHADOW_TYPE_DELTA) && shadow->delta) { - LOG_DBG("Shadow: Delta - version: %d, timestamp: %lld", - shadow->delta->ver, - shadow->delta->ts); - bool accept = true; err = shadow_config_delta_process(&shadow->delta->state); @@ -385,6 +390,31 @@ static void handle_shadow_event(struct nrf_cloud_obj_shadow_data *const shadow) return; } + if (!shadow->delta->state.encoded_data.ptr && + (shadow->delta->state.enc_src != NRF_CLOUD_ENC_SRC_NONE)) { + LOG_ERR("Encoded data ptr is NULL"); + return; + } + if (!shadow->delta->state.json) { + LOG_ERR("JSON is NULL"); + return; + } + LOG_INF("Shadow: Delta - version: %d, timestamp: %lld, %*s", + shadow->delta->ver, + shadow->delta->ts, + shadow->delta->state.encoded_data.len, + (shadow->delta->state.enc_src != NRF_CLOUD_ENC_SRC_NONE) ? + (const char *)shadow->delta->state.encoded_data.ptr : + "(not encoded)"); + +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + LOG_INF("Processing gateway connection changes"); + err = gateway_shadow_delta_handler(shadow->delta); + if (err) { + LOG_ERR("Error handling delta: %d", err); + } +#endif + err = nrf_cloud_obj_shadow_delta_response_encode(&shadow->delta->state, accept); if (err) { LOG_ERR("Failed to encode shadow response: %d", err); @@ -403,6 +433,16 @@ static void handle_shadow_event(struct nrf_cloud_obj_shadow_data *const shadow) /* Send the config on an error */ (void)shadow_config_reported_send(); } +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + LOG_INF("Requesting gateway desired connections"); + (void)gateway_shadow_accepted_handler(&shadow->accepted->desired); +#endif + } else if ((shadow->type == NRF_CLOUD_OBJ_SHADOW_TYPE_TF) && shadow->transform) { + LOG_DBG("Shadow: Transform"); +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + LOG_INF("Processing gateway desired connections"); + (void)gateway_shadow_transform_handler(&shadow->transform->result.obj); +#endif } } @@ -435,6 +475,9 @@ static void cloud_event_handler(const struct nrf_cloud_evt *nrf_cloud_evt) * succeeds. */ LOG_INF("Please add this device to your cloud account in the nRF Cloud portal."); +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + reset_gateway(); +#endif /* Store the fact that this is an initial association. * @@ -501,7 +544,9 @@ static void cloud_event_handler(const struct nrf_cloud_evt *nrf_cloud_evt) */ general_dev_msg_handler(&nrf_cloud_evt->data); } - +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + gateway_data_handler(&nrf_cloud_evt->data); +#endif break; case NRF_CLOUD_EVT_RX_DATA_DISCON: LOG_DBG("NRF_CLOUD_EVT_RX_DATA_DISCON"); @@ -510,6 +555,8 @@ static void cloud_event_handler(const struct nrf_cloud_evt *nrf_cloud_evt) break; case NRF_CLOUD_EVT_RX_DATA_SHADOW: { LOG_DBG("NRF_CLOUD_EVT_RX_DATA_SHADOW"); + LOG_DBG("shadow: %*s", nrf_cloud_evt->data.len, + (const char *)nrf_cloud_evt->data.ptr); handle_shadow_event(nrf_cloud_evt->shadow); break; } @@ -590,6 +637,13 @@ static int setup_cloud(void) return err; } +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + err = lte_lc_psm_req(IS_ENABLED(CONFIG_LTE_PSM_REQ)); + if (err) { + LOG_ERR("Unable to disable PSM: %d", err); + } +#endif + #elif defined(CONFIG_NRF_CLOUD_COAP) #if defined(CONFIG_COAP_FOTA) err = coap_fota_init(); @@ -613,6 +667,11 @@ static void check_credentials(void) { int status = nrf_cloud_credentials_configured_check(); +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + if (status) { + reset_gateway(); + } +#endif if (status == -ENOTSUP) { if (IS_ENABLED(CONFIG_NRF_PROVISIONING)) { LOG_WRN("nRF Cloud credentials are not installed. " diff --git a/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.h b/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.h index 7a07b23a4c5f..d06960ee1794 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.h +++ b/samples/cellular/nrf_cloud_multi_service/src/cloud_connection.h @@ -75,6 +75,11 @@ void register_general_dev_msg_handler(dev_msg_handler_cb_t handler_cb); */ void disconnect_cloud(void); +/** + * @brief Allow automatic reconnections to the cloud. + */ +void control_cloud_connection(bool enable); + /** * @brief Report a communications error so the cloud_connection can evaluate and * recover. diff --git a/samples/cellular/nrf_cloud_multi_service/src/fota_support.c b/samples/cellular/nrf_cloud_multi_service/src/fota_support.c index 393fb2072e77..b8adef3b3dbe 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/fota_support.c +++ b/samples/cellular/nrf_cloud_multi_service/src/fota_support.c @@ -17,7 +17,7 @@ void on_fota_downloaded(void) sample_reboot_normal(); } -struct dfu_target_fmfu_fdev * get_full_modem_fota_fdev(void) +struct dfu_target_fmfu_fdev *get_full_modem_fota_fdev(void) { if (IS_ENABLED(CONFIG_NRF_CLOUD_FOTA_FULL_MODEM_UPDATE)) { static struct dfu_target_fmfu_fdev ext_flash_dev = { diff --git a/samples/cellular/nrf_cloud_multi_service/src/fota_support.h b/samples/cellular/nrf_cloud_multi_service/src/fota_support.h index 389fd06c4e61..b0d65ed4bbed 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/fota_support.h +++ b/samples/cellular/nrf_cloud_multi_service/src/fota_support.h @@ -31,6 +31,6 @@ void on_fota_downloaded(void); * enabled. * */ -struct dfu_target_fmfu_fdev * get_full_modem_fota_fdev(void); +struct dfu_target_fmfu_fdev *get_full_modem_fota_fdev(void); #endif /* _FOTA_SUPPORT_H_ */ diff --git a/samples/cellular/nrf_cloud_multi_service/src/main.c b/samples/cellular/nrf_cloud_multi_service/src/main.c index ba6eac5c69e1..b7082589ad83 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/main.c +++ b/samples/cellular/nrf_cloud_multi_service/src/main.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2023 Nordic Semiconductor ASA +/* Copyright (c) 2024 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause */ @@ -65,6 +65,9 @@ int main(void) LOG_INF("nRF Cloud multi-service sample has started, version: %s, protocol: %s", CONFIG_APP_VERSION, protocol); +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + LOG_INF("nRF Cloud BLE gateway support is enabled."); +#endif return 0; } diff --git a/samples/cellular/nrf_cloud_multi_service/src/message_queue.c b/samples/cellular/nrf_cloud_multi_service/src/message_queue.c index fcc35ebc2c65..1a7f0b2bb31f 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/message_queue.c +++ b/samples/cellular/nrf_cloud_multi_service/src/message_queue.c @@ -19,7 +19,7 @@ LOG_MODULE_REGISTER(message_queue, CONFIG_MULTI_SERVICE_LOG_LEVEL); -/* Message Queue for enqueing outgoing messages during offline periods. */ +/* Message Queue for enqueuing outgoing messages during offline periods. */ K_MSGQ_DEFINE(device_message_queue, sizeof(struct nrf_cloud_obj *), CONFIG_MAX_OUTGOING_MESSAGES, diff --git a/samples/cellular/nrf_cloud_multi_service/src/shadow_support_coap.c b/samples/cellular/nrf_cloud_multi_service/src/shadow_support_coap.c index 8c8b6e48b9bb..0ed65a14e4f2 100644 --- a/samples/cellular/nrf_cloud_multi_service/src/shadow_support_coap.c +++ b/samples/cellular/nrf_cloud_multi_service/src/shadow_support_coap.c @@ -54,6 +54,12 @@ static int process_delta(struct nrf_cloud_data *const delta) return err; } +#if defined(CONFIG_NRF_CLOUD_GATEWAY) + if (delta_obj.type == NRF_CLOUD_OBJ_TYPE_JSON) { + err = gateway_state_handler(delta_obj.json); + } +#endif + /* Reject the delta by updating desired. * Accept the delta by updating reported. */ diff --git a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/apricity_gateway_nrf9160.conf b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/apricity_gateway_nrf9160.conf new file mode 100644 index 000000000000..b992af6919ee --- /dev/null +++ b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/apricity_gateway_nrf9160.conf @@ -0,0 +1,32 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_SPI=y +CONFIG_SPI_NOR=y +CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 +CONFIG_SPI_NRFX_RAM_BUFFER_SIZE=32 + +CONFIG_PM_OVERRIDE_EXTERNAL_DRIVER_CHECK=y + +CONFIG_FPROTECT=n + +# Enabling SPI increases the MCUBoot image size so that it does not fit in the default +# partition size (0xC000). The minimum required size is 0xD000 +CONFIG_PM_PARTITION_SIZE_MCUBOOT=0xD000 + +# MCUboot requires a large stack size, otherwise an MPU fault will occur +CONFIG_MAIN_STACK_SIZE=16384 + +# Enable flash operations +CONFIG_FLASH=y + +CONFIG_BOOT_MAX_IMG_SECTORS=256 + +CONFIG_MULTITHREADING=y + +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DEFAULT_LEVEL=0 diff --git a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9160dk_nrf9160_0_14_0.overlay b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9160dk_nrf9160_0_14_0.overlay index 3a592b306b73..f9deaa4c0ddc 100644 --- a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9160dk_nrf9160_0_14_0.overlay +++ b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9160dk_nrf9160_0_14_0.overlay @@ -8,6 +8,8 @@ chosen { zephyr,code-partition = &boot_partition; nordic,pm-ext-flash = &mx25r64; + /* zephyr,bt-uart=&lpuart; */ + zephyr,bt-hci=&bt_hci_uart; }; }; @@ -15,3 +17,49 @@ &mx25r64 { status = "okay"; }; + +&nrf52840_reset { + status = "okay"; +}; + +&gpiote { + interrupts = <49 NRF_DEFAULT_IRQ_PRIORITY>; +}; + +&uart2 { + current-speed = <1000000>; + status = "okay"; + /delete-property/ hw-flow-control; + + pinctrl-0 = <&uart2_default_alt>; + pinctrl-1 = <&uart2_sleep_alt>; + pinctrl-names = "default", "sleep"; + bt_hci_uart: bt_hci_uart { + compatible = "zephyr,bt-hci-uart"; + status = "okay"; + /*rts-pin = <21>; <&interface_to_nrf52840 3 0>; */ + /*cts-pin = <19>; <&interface_to_nrf52840 2 0>; */ + }; +}; + +&pinctrl { + uart2_default_alt: uart2_default_alt { + group1 { + psels = , + , + , + ; + }; + }; + + uart2_sleep_alt: uart2_sleep_alt { + group1 { + psels = , + , + , + ; + low-power-enable; + }; + }; + +}; diff --git a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9161dk_nrf9161_0_7_0.overlay b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9161dk_nrf9161_0_7_0.overlay index 26c468ea5b44..3f2322d387cf 100644 --- a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9161dk_nrf9161_0_7_0.overlay +++ b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/boards/nrf9161dk_nrf9161_0_7_0.overlay @@ -15,4 +15,4 @@ &gd25lb256 { status = "okay"; - }; +}; diff --git a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/prj.conf b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/prj.conf index 394a6e6dc20d..2b1d662c393c 100644 --- a/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/prj.conf +++ b/samples/cellular/nrf_cloud_multi_service/sysbuild/mcuboot/prj.conf @@ -21,8 +21,8 @@ CONFIG_BOOT_BOOTSTRAP=n CONFIG_FLASH=y CONFIG_FPROTECT=y +CONFIG_BT=y ### Various Zephyr boards enable features that we don't want. -# CONFIG_BT is not set # CONFIG_BT_CTLR is not set # CONFIG_I2C is not set