Memfault Firmware SDK 1.32.0 (Build 15941)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d74f4ba..4a1ebb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,74 @@
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.32.0] - 2025-12-03
+
+This is a minor release. Key updates:
+
+- Integrated fuel gauge battery metrics for nPM13xx PMICs on nRF Connect SDK.
+- Added CoAP client for uploading Memfault data via nRF Cloud.
+- Fixed build issues and compiler warnings on Zephyr and nRF Connect SDK.
+
+### 📈 Added
+
+- nRF-Connect SDK:
+
+ - Add a battery metrics port for the nPM1300 and nPM1304 PMICs, which includes
+ collecting battery metrics for the
+ [Battery Device Vital](https://docs.memfault.com/docs/platform/memfault-core-metrics?platform=MCU#battery)
+ and a heartbeat metric for battery voltage. To leverage this port, set
+ `CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX=y`. Note that users must
+ provide the header `memfault_nrf_platform_battery_model.h`, which should
+ define the battery model as specified by the nRF Fuel Gauge API. See the
+ [Nordic docs](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nrf_fuel_gauge/README.html#nrf-fuel-gauge)
+ for more detail. Since this port calls `nrf_fuel_gauge_process()`,
+ applications that want to read state of charge should call
+ `memfault_platform_get_stateofcharge()` to avoid conflicting calls to the
+ fuel gauge library.
+
+ - Added a CoAP client implementation capable of uploading Memfault data
+ through an [nRF Cloud](https://www.nrfcloud.com/) connection. This is
+ primarily intended for use with the Nordic nRF91x series devices using LTE-M
+ or NB-IoT connectivity. To enable, use
+ `CONFIG_MEMFAULT_USE_NRF_CLOUD_TRANSPORT=y`. This will change the protocol
+ used by `memfault_zephyr_port_post_data()` (and
+ `CONFIG_MEMFAULT_HTTP_PERIODIC_UPLOAD`), from HTTP to CoAP.
+
+- Zephyr:
+
+ - Add the symbol `CONFIG_MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE` with a
+ default value of 1000, which maps to 3 decimal places of precision for
+ battery metrics. See the
+ [Battery Device Vital docs](https://docs.memfault.com/docs/platform/memfault-core-metrics?platform=MCU#battery)
+ for more information on configuring battery metric collection.
+
+### 🐛 Fixed
+
+- Zephyr:
+
+ - Fix a compiler warning on Zephyr v4.1, when using
+ `CONFIG_MEMFAULT_METRICS_WIFI`. Thanks to
+ [@chshzh](https://github.com/chshzh) for reporting this issue in
+ [#98](https://github.com/memfault/memfault-firmware-sdk/issues/98) 🎉!
+
+ - Fix an incorrect check for the Kconfig option
+ `CONFIG_MEMFAULT_FAULT_HANDLER_LOG_PANIC` (previously was checking for
+ `defined(MEMFAULT_FAULT_HANDLER_LOG_PANIC)`, which is incorrect). Thanks to
+ [@konstk1](https://github.com/konstk1) for providing this fix in
+ [#100](https://github.com/memfault/memfault-firmware-sdk/pull/100) 🎉!
+
+- General:
+
+ - Fix a few files that were missing necessary `#include <stdio.h>` or
+ `#include <string.h>` directives.
+
+- nRF Connect SDK:
+
+ - Fix a compilation error when building for the nRF53 series on nRF Connect
+ SDK v3.2.0-rc1 and later, caused by a change in the NRFX HAL. Thanks to
+ [@Damian-Nordic](https://github.com/Damian-Nordic) for providing the fix in
+ [#99](https://github.com/memfault/memfault-firmware-sdk/pull/99) 🎉!
+
## [1.31.0] - 2025-11-22
This is a minor feature and bugfix release. Key updates:
diff --git a/VERSION b/VERSION
index 83e5478..b97a005 100644
--- a/VERSION
+++ b/VERSION
@@ -1,3 +1,3 @@
-BUILD ID: 15800
-GIT COMMIT: ab868032c5
-VERSION: 1.31.0
+BUILD ID: 15941
+GIT COMMIT: f62a776715
+VERSION: 1.32.0
diff --git a/components/include/memfault/version.h b/components/include/memfault/version.h
index 55b22fa..9e49895 100644
--- a/components/include/memfault/version.h
+++ b/components/include/memfault/version.h
@@ -20,8 +20,8 @@
} sMfltSdkVersion;
#define MEMFAULT_SDK_VERSION \
- { .major = 1, .minor = 31, .patch = 0 }
-#define MEMFAULT_SDK_VERSION_STR "1.31.0"
+ { .major = 1, .minor = 32, .patch = 0 }
+#define MEMFAULT_SDK_VERSION_STR "1.32.0"
#ifdef __cplusplus
}
diff --git a/components/metrics/src/memfault_metrics_battery.c b/components/metrics/src/memfault_metrics_battery.c
index 44b0c3b..66ab9e7 100644
--- a/components/metrics/src/memfault_metrics_battery.c
+++ b/components/metrics/src/memfault_metrics_battery.c
@@ -32,7 +32,13 @@
(int32_t)s_memfault_battery_ctx.last_state_of_charge - (int32_t)current_stateofcharge;
if (drop < 0) {
- MEMFAULT_LOG_DEBUG("Battery drop was negative (%" PRIi32 "), skipping", drop);
+ // Format drop as percentage with max 3 decimal places
+ const int32_t scale = MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE;
+ const int32_t integer_part = -drop / scale;
+ const uint32_t fractional_part =
+ (((uint32_t)(-drop) % (uint32_t)scale) * 1000) / (uint32_t)scale;
+ MEMFAULT_LOG_DEBUG("Battery drop was negative (-%" PRIi32 ".%03" PRIu32 "%%), skipping",
+ integer_part, fractional_part);
}
// compute elapsed time since last data collection
diff --git a/examples/esp32/apps/memfault_demo_app/main/app_memfault_transport_mqtt.c b/examples/esp32/apps/memfault_demo_app/main/app_memfault_transport_mqtt.c
index 19e23a4..908f5f3 100644
--- a/examples/esp32/apps/memfault_demo_app/main/app_memfault_transport_mqtt.c
+++ b/examples/esp32/apps/memfault_demo_app/main/app_memfault_transport_mqtt.c
@@ -5,6 +5,8 @@
#include <stddef.h>
#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
#include "app_memfault_transport.h"
#include "esp_log.h"
diff --git a/ports/esp_idf/memfault/common/memfault_platform_core.c b/ports/esp_idf/memfault/common/memfault_platform_core.c
index 0116d7d..a1f4702 100644
--- a/ports/esp_idf/memfault/common/memfault_platform_core.c
+++ b/ports/esp_idf/memfault/common/memfault_platform_core.c
@@ -6,6 +6,7 @@
#include <inttypes.h>
#include <stdarg.h>
#include <stdatomic.h>
+#include <stdio.h>
#include <stdlib.h>
#include "esp_idf_version.h"
diff --git a/ports/esp_idf/memfault/common/memfault_platform_http_client.c b/ports/esp_idf/memfault/common/memfault_platform_http_client.c
index 5d4b9da..c7dad11 100644
--- a/ports/esp_idf/memfault/common/memfault_platform_http_client.c
+++ b/ports/esp_idf/memfault/common/memfault_platform_http_client.c
@@ -10,6 +10,7 @@
#if MEMFAULT_ESP_HTTP_CLIENT_ENABLE
+ #include <stdio.h>
#include <string.h>
#include "esp_http_client.h"
diff --git a/ports/esp_idf/memfault/common/memfault_platform_metrics.c b/ports/esp_idf/memfault/common/memfault_platform_metrics.c
index 72274c3..c6adf2a 100644
--- a/ports/esp_idf/memfault/common/memfault_platform_metrics.c
+++ b/ports/esp_idf/memfault/common/memfault_platform_metrics.c
@@ -10,6 +10,7 @@
//! -DMEMFAULT_METRICS_HEARTBEAT_INTERVAL_SECS=15
#include <inttypes.h>
+#include <stdio.h>
#include <string.h>
#include "esp_err.h"
diff --git a/ports/zephyr/Kconfig b/ports/zephyr/Kconfig
index 46da5bc..4342623 100644
--- a/ports/zephyr/Kconfig
+++ b/ports/zephyr/Kconfig
@@ -732,6 +732,18 @@
help
Automatically initialize the battery metric subsystem on bootup
+config MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE
+ int "Scale factor for battery state of charge percentage metric"
+ range 1 1000
+ default 1000
+ depends on MEMFAULT_METRICS_BATTERY_ENABLE
+ help
+ The scale factor to use for the battery state of charge percentage metric.
+ The value recorded is multiplied by this factor before being stored in the metric.
+ For example, if the battery state of charge is 75.00%, and the scale factor is 100,
+ the value stored in the metric will be 7500, and is scaled back to 75.00% when
+ Memfault's cloud processes the data.
+
config MEMFAULT_METRICS_TCP_IP
bool "TCP/IP metrics"
default y
diff --git a/ports/zephyr/common/CMakeLists.txt b/ports/zephyr/common/CMakeLists.txt
index 45fdd4b..293adec 100644
--- a/ports/zephyr/common/CMakeLists.txt
+++ b/ports/zephyr/common/CMakeLists.txt
@@ -14,8 +14,12 @@
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_SHELL memfault_demo_cli.c)
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_SHELL_SELF_TEST memfault_self_test_platform.c)
+if(CONFIG_MEMFAULT_HTTP_ENABLE OR CONFIG_USE_NRF_CLOUD_TRANSPORT)
+ zephyr_library_sources(memfault_platform_post.c)
+endif()
+
if (NOT CONFIG_MEMFAULT_SOFTWARE_WATCHDOG_CUSTOM)
- zephyr_library_sources(memfault_software_watchdog.c)
+zephyr_library_sources(memfault_software_watchdog.c)
endif()
if (CONFIG_MEMFAULT_CACHE_FAULT_REGS)
diff --git a/ports/zephyr/common/memfault_platform_http.c b/ports/zephyr/common/memfault_platform_http.c
index a6b747c..6fa8f4a 100644
--- a/ports/zephyr/common/memfault_platform_http.c
+++ b/ports/zephyr/common/memfault_platform_http.c
@@ -704,7 +704,7 @@
return success ? 1 : -1;
}
-ssize_t memfault_zephyr_port_post_data_return_size(void) {
+ssize_t memfault_zephyr_port_http_post_data_return_size(void) {
if (!memfault_packetizer_data_available()) {
return 0;
}
@@ -730,11 +730,6 @@
return (rv == 0) ? (ctx.bytes_sent) : rv;
}
-int memfault_zephyr_port_post_data(void) {
- ssize_t rv = memfault_zephyr_port_post_data_return_size();
- return (rv >= 0) ? 0 : rv;
-}
-
int memfault_zephyr_port_http_open_socket(sMemfaultHttpContext *ctx) {
const char *host = MEMFAULT_HTTP_GET_CHUNKS_API_HOST();
const int port = MEMFAULT_HTTP_GET_CHUNKS_API_PORT();
diff --git a/ports/zephyr/common/memfault_platform_post.c b/ports/zephyr/common/memfault_platform_post.c
new file mode 100644
index 0000000..93e9ebf
--- /dev/null
+++ b/ports/zephyr/common/memfault_platform_post.c
@@ -0,0 +1,29 @@
+//! @file
+//!
+//! Copyright (c) Memfault, Inc.
+//! See LICENSE for details
+//!
+//! @brief
+//! Selects which backend to use for posting data.
+
+#include "memfault/ports/zephyr/http.h"
+
+#if defined(CONFIG_MEMFAULT_USE_NRF_CLOUD_TRANSPORT)
+ // NCS v1.9.2 does not have a recent enough Zephyr version to include this
+ // header. This Kconfig option is blocked at CMake stage, so here it is safe
+ // to include.
+ #include "memfault/nrfconnect_port/coap.h"
+#endif
+
+ssize_t memfault_zephyr_port_post_data_return_size(void) {
+#if defined(CONFIG_MEMFAULT_USE_NRF_CLOUD_TRANSPORT)
+ return memfault_zephyr_port_coap_post_data_return_size();
+#else // default to HTTP transport
+ return memfault_zephyr_port_http_post_data_return_size();
+#endif
+}
+
+int memfault_zephyr_port_post_data(void) {
+ ssize_t rv = memfault_zephyr_port_post_data_return_size();
+ return (rv >= 0) ? 0 : rv;
+}
diff --git a/ports/zephyr/common/metrics/memfault_platform_metrics.c b/ports/zephyr/common/metrics/memfault_platform_metrics.c
index 8cffdb9..03f6f29 100644
--- a/ports/zephyr/common/metrics/memfault_platform_metrics.c
+++ b/ports/zephyr/common/metrics/memfault_platform_metrics.c
@@ -14,6 +14,7 @@
#endif
#include <stdbool.h>
+#include <stdio.h>
#include "memfault/core/debug_log.h"
#include "memfault/metrics/metrics.h"
diff --git a/ports/zephyr/common/metrics/memfault_platform_wifi_metrics.c b/ports/zephyr/common/metrics/memfault_platform_wifi_metrics.c
index 1627a78..2be595d 100644
--- a/ports/zephyr/common/metrics/memfault_platform_wifi_metrics.c
+++ b/ports/zephyr/common/metrics/memfault_platform_wifi_metrics.c
@@ -147,7 +147,7 @@
}
static void prv_wifi_event_callback(struct net_mgmt_event_callback *cb,
-#if MEMFAULT_ZEPHYR_VERSION_GT(4, 1)
+#if MEMFAULT_ZEPHYR_VERSION_GT(4, 2)
uint64_t mgmt_event,
#else
uint32_t mgmt_event,
diff --git a/ports/zephyr/config/memfault_metrics_heartbeat_zephyr_port_config.def b/ports/zephyr/config/memfault_metrics_heartbeat_zephyr_port_config.def
index afd7afe..d20edeb 100644
--- a/ports/zephyr/config/memfault_metrics_heartbeat_zephyr_port_config.def
+++ b/ports/zephyr/config/memfault_metrics_heartbeat_zephyr_port_config.def
@@ -113,6 +113,10 @@
#include "memfault_metrics_heartbeat_extra.def"
#endif
+#if defined(CONFIG_MEMFAULT_NRF_CONNECT_SDK)
+ #include "memfault_metrics_heartbeat_ncs_port_config.def"
+#endif
+
// Pull in the user's heartbeat defs
#if defined(CONFIG_MEMFAULT_USER_CONFIG_ENABLE)
diff --git a/ports/zephyr/config/memfault_zephyr_platform_config.h b/ports/zephyr/config/memfault_zephyr_platform_config.h
index d7dc85e..41ebc48 100644
--- a/ports/zephyr/config/memfault_zephyr_platform_config.h
+++ b/ports/zephyr/config/memfault_zephyr_platform_config.h
@@ -83,6 +83,11 @@
#define MEMFAULT_METRICS_BATTERY_ENABLE 1
#endif
+#if defined(CONFIG_MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE)
+ #define MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE \
+ CONFIG_MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE
+#endif
+
#if defined(CONFIG_MEMFAULT_SHELL_SELF_TEST)
#define MEMFAULT_DEMO_CLI_SELF_TEST 1
#define MEMFAULT_SELF_TEST_COREDUMP_STORAGE_DISABLE_MSG \
diff --git a/ports/zephyr/include/memfault/ports/zephyr/http.h b/ports/zephyr/include/memfault/ports/zephyr/http.h
index 2d1be0b..e26e413 100644
--- a/ports/zephyr/include/memfault/ports/zephyr/http.h
+++ b/ports/zephyr/include/memfault/ports/zephyr/http.h
@@ -42,6 +42,12 @@
//! sent, in bytes. 0 indicates no data was ready to send (and no data was sent)
ssize_t memfault_zephyr_port_post_data_return_size(void);
+//! HTTP specific version of memfault_zephyr_port_post_data_return_size
+//!
+//! @return negative error code on error, else the size of the data that was
+//! sent, in bytes. 0 indicates no data was ready to send (and no data was sent)
+ssize_t memfault_zephyr_port_http_post_data_return_size(void);
+
typedef struct MemfaultOtaInfo {
// The size, in bytes, of the OTA payload.
size_t size;
diff --git a/ports/zephyr/include/memfault/ports/zephyr/log_panic.h b/ports/zephyr/include/memfault/ports/zephyr/log_panic.h
index 098842b..6f5aa71 100644
--- a/ports/zephyr/include/memfault/ports/zephyr/log_panic.h
+++ b/ports/zephyr/include/memfault/ports/zephyr/log_panic.h
@@ -11,7 +11,7 @@
extern "C" {
#endif
-#if defined(MEMFAULT_FAULT_HANDLER_LOG_PANIC)
+#if defined(CONFIG_MEMFAULT_FAULT_HANDLER_LOG_PANIC)
#define MEMFAULT_LOG_PANIC() LOG_PANIC()
#else
#define MEMFAULT_LOG_PANIC()
diff --git a/ports/zephyr/ncs/CMakeLists.txt b/ports/zephyr/ncs/CMakeLists.txt
index c1edab5..3d796ce 100644
--- a/ports/zephyr/ncs/CMakeLists.txt
+++ b/ports/zephyr/ncs/CMakeLists.txt
@@ -8,6 +8,7 @@
add_subdirectory(src)
zephyr_include_directories(include)
+ zephyr_include_directories(config)
# Note: Starting in nRF Connect SDK >= 1.3, NCS_VERSION_* fields are exposed.
# Prior to that, they're unset and cause the VERSION_GREATER_EQUAL check to be
diff --git a/ports/zephyr/ncs/Kconfig b/ports/zephyr/ncs/Kconfig
index fc3d53b..9329621 100644
--- a/ports/zephyr/ncs/Kconfig
+++ b/ports/zephyr/ncs/Kconfig
@@ -76,4 +76,62 @@
Enables collection of CPU temperature metrics for supported devices.
Use the MEMFAULT_METRICS_CPU_TEMP config option to disable support.
+config MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX
+ bool "nPM13xx Battery Support"
+ default y if BOARD_THINGY91X_NRF9151_NS
+ select MEMFAULT_METRICS_BATTERY_ENABLE
+ depends on NRF_FUEL_GAUGE
+ depends on NPM13XX_CHARGER
+ help
+ Enables battery metrics support for devices using the nPM13xx PMIC.
+ The user must provide memfault_nrf_platform_battery_model.h with the
+ model of the battery as defined by the nRF Fuel Gauge API.
+ Note, since this port calls nrf_fuel_gauge_process(), applications
+ that want to read state of charge should call
+ memfault_platform_get_stateofcharge() to avoid conflicting calls to
+ the fuel gauge library.
+
+config MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX_INIT_PRIORITY
+ int "nPM13xx Battery Initialization Priority"
+ # Set to the default for MEMFAULT_INIT_PRIORITY - 1
+ default 39
+ depends on MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX
+ help
+ Sets the initialization priority for the nPM13xx battery platform
+ integration. This value is should be set to be lower than
+ MEMFAULT_INIT_PRIORITY (higher priority) to ensure that the platform
+ battery SOC can be collected during Memfault initialization.
+
+menuconfig MEMFAULT_USE_NRF_CLOUD_TRANSPORT
+ bool "Use nRF Cloud CoAP endpoint"
+ depends on MODEM_JWT
+ depends on COAP
+ depends on (COAP_EXTENDED_OPTIONS_LEN_VALUE >= 192)
+ help
+ Note: Requires device to be provisioned with nRF Cloud.
+
+if MEMFAULT_USE_NRF_CLOUD_TRANSPORT
+
+config MEMFAULT_NRF_CLOUD_SEC_TAG
+ int "Security tag to use for nRF Cloud connection"
+ default 16842753
+
+config MEMFAULT_NRF_CLOUD_HOST_NAME
+ string "nRF Cloud server hostname"
+ default "coap.nrfcloud.com"
+
+config MEMFAULT_COAP_PACKETIZER_BUFFER_SIZE
+ int "Set the size of the CoAP packetizer buffer"
+ default 1400
+
+config MEMFAULT_COAP_CLIENT_TIMEOUT_MS
+ int "The CoAP client timeout in milliseconds"
+ default 5000
+ help
+ The Memfault CoAP client timeout in milliseconds. This is the
+ maximum amount of time the CoAP client will wait for a response from
+ the server.
+
+endif # MEMFAULT_USE_NRF_CLOUD_TRANSPORT
+
endif # MEMFAULT_NRF_CONNECT_SDK
diff --git a/ports/zephyr/ncs/config/memfault_metrics_heartbeat_ncs_port_config.def b/ports/zephyr/ncs/config/memfault_metrics_heartbeat_ncs_port_config.def
new file mode 100644
index 0000000..6bd991a
--- /dev/null
+++ b/ports/zephyr/ncs/config/memfault_metrics_heartbeat_ncs_port_config.def
@@ -0,0 +1,3 @@
+#if defined(CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX)
+MEMFAULT_METRICS_KEY_DEFINE_WITH_SCALE_VALUE(battery_voltage, kMemfaultMetricType_Unsigned, 1000)
+#endif
diff --git a/ports/zephyr/ncs/include/memfault/nrfconnect_port/coap.h b/ports/zephyr/ncs/include/memfault/nrfconnect_port/coap.h
new file mode 100644
index 0000000..ddc2c2d
--- /dev/null
+++ b/ports/zephyr/ncs/include/memfault/nrfconnect_port/coap.h
@@ -0,0 +1,74 @@
+#pragma once
+
+//! @file
+//!
+//! Copyright (c) Memfault, Inc.
+//! See LICENSE for details
+//!
+//! @brief
+//! Zephyr specific coap utility for interfacing with Memfault CoAP utilities
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include "memfault/ports/zephyr/include_compatibility.h"
+
+#include MEMFAULT_ZEPHYR_INCLUDE(net/coap.h)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//! Context structure used to carry state information about the CoAP connection
+typedef struct {
+ int sock_fd;
+ struct zsock_addrinfo *res;
+ size_t bytes_sent;
+ uint8_t message_token[COAP_TOKEN_MAX_LEN];
+} sMemfaultCoAPContext;
+
+//! Open a socket to the Memfault chunks upload server
+//!
+//! This function is the simplest way to connect to Memfault. Internally this function combines the
+//! required socket operations into a single function call. See the other socket functions for
+//! advanced usage.
+//!
+//! @param ctx If the socket is opened successfully, this will be populated with
+//! the connection state for the other COAP functions below
+//!
+//! @note After use, memfault_zephyr_port_coap_close_socket() must be called
+//! to close the socket and free any associated memory.
+//! @note In the case of an error, it is not required to call memfault_zephyr_port_coap_close_socket
+//!
+//! @return
+//! 0 : Success
+//! < 0 : Error
+int memfault_zephyr_port_coap_open_socket(sMemfaultCoAPContext *ctx);
+
+//! Close a socket previously opened with
+//! memfault_zephyr_port_coap_open_socket()
+void memfault_zephyr_port_coap_close_socket(sMemfaultCoAPContext *ctx);
+
+//! Identical to memfault_zephyr_port_post_data() but uses the already-opened
+//! socket to send data
+//!
+//! @param ctx Connection context previously opened with
+//! memfault_zephyr_port_coap_open_socket()
+//!
+//! @return 0 on success, -1 on error
+int memfault_zephyr_port_coap_upload_sdk_data(sMemfaultCoAPContext *ctx);
+
+//! CoAP specific version of memfault_zephyr_port_post_data_return_size
+//!
+//! @return negative error code on error, else the size of the data that was
+//! sent, in bytes. 0 indicates no data was ready to send (and no data was sent)
+ssize_t memfault_zephyr_port_coap_post_data_return_size(void);
+
+#define MEMFAULT_NRF_CLOUD_COAP_PORT 5684
+#define MEMFAULT_NRF_CLOUD_COAP_PROJECT_KEY_OPTION_NO 2429
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/ports/zephyr/ncs/src/CMakeLists.txt b/ports/zephyr/ncs/src/CMakeLists.txt
index 5a5b3e7..c9de8e8 100644
--- a/ports/zephyr/ncs/src/CMakeLists.txt
+++ b/ports/zephyr/ncs/src/CMakeLists.txt
@@ -1,6 +1,7 @@
# Controls where root certificates are stored
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_ROOT_CERT_STORAGE_NRF9160_MODEM memfault_nrf91_root_cert_storage.c)
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_REBOOT_REASON_GET_CUSTOM nrfx_pmu_reboot_tracking.c)
+zephyr_library_sources_ifdef(CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX memfault_platform_npm13xx_battery.c)
if (${NCS_VERSION_MAJOR}.${NCS_VERSION_MINOR}.${NCS_VERSION_PATCH} VERSION_GREATER_EQUAL 2.9.99)
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_FOTA memfault_fota.c)
@@ -10,3 +11,14 @@
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_NRF_SHELL memfault_nrf_cli.c)
zephyr_library_sources_ifdef(CONFIG_MEMFAULT_NRF_CONNECTIVITY_CONNECTED_TIME_NRF91X memfault_platform_metrics_connectivity_lte.c)
+
+if (CONFIG_MEMFAULT_USE_NRF_CLOUD_TRANSPORT)
+ # if Zephyr is less than 3.5.0, print an error
+ if(${KERNEL_VERSION_MAJOR}.${KERNEL_VERSION_MINOR}.${KERNEL_VERSION_PATCH} VERSION_LESS "3.5.0")
+ message(FATAL_ERROR
+ "Memfault nRF Cloud CoAP transport requires Zephyr version 3.5.0 or newer."
+ )
+ endif()
+
+ zephyr_library_sources(memfault_platform_coap.c)
+endif()
diff --git a/ports/zephyr/ncs/src/memfault_platform_coap.c b/ports/zephyr/ncs/src/memfault_platform_coap.c
new file mode 100644
index 0000000..05d918e
--- /dev/null
+++ b/ports/zephyr/ncs/src/memfault_platform_coap.c
@@ -0,0 +1,555 @@
+//! @file
+//!
+//! Copyright (c) Memfault, Inc.
+//! See LICENSE for details
+//!
+
+#include MEMFAULT_ZEPHYR_INCLUDE(kernel.h)
+#include MEMFAULT_ZEPHYR_INCLUDE(net/coap.h)
+#include MEMFAULT_ZEPHYR_INCLUDE(net/socket.h)
+#include <date_time.h>
+#include <memfault/metrics/connectivity.h>
+#include <modem/modem_jwt.h>
+#include <nrf_modem_at.h>
+#include <zephyr/random/random.h>
+
+#include "memfault/core/data_packetizer.h"
+#include "memfault/core/debug_log.h"
+#include "memfault/core/platform/device_info.h"
+#include "memfault/http/http_client.h"
+#include "memfault/http/utils.h"
+#include "memfault/nrfconnect_port/coap.h"
+#include "memfault/ports/zephyr/http.h"
+
+extern sMfltHttpClientConfig g_mflt_http_client_config;
+
+#if defined(CONFIG_MEMFAULT_NCS_PROJECT_KEY)
+ #undef CONFIG_MEMFAULT_PROJECT_KEY
+ #define CONFIG_MEMFAULT_PROJECT_KEY CONFIG_MEMFAULT_NCS_PROJECT_KEY
+#endif // defined(CONFIG_MEMFAULT_NCS_PROJECT_KEY)
+
+static int prv_generate_jwt(char *jwt_buf, size_t jwt_buf_sz, int sec_tag) {
+ int err = 0;
+ int retry_count = 0;
+
+ // wait until modem has valid time
+ const int max_retries = 60; // 60 seconds max wait
+ while (nrf_modem_at_cmd(jwt_buf, jwt_buf_sz, "AT%%CCLK?")) {
+ if (++retry_count >= max_retries) {
+ return -ETIMEDOUT;
+ }
+ k_sleep(K_SECONDS(1));
+ }
+
+ struct jwt_data jwt = {
+ .audience = NULL,
+ .key = JWT_KEY_TYPE_CLIENT_PRIV,
+ .alg = JWT_ALG_TYPE_ES256,
+ .jwt_buf = jwt_buf,
+ .jwt_sz = jwt_buf_sz,
+ .exp_delta_s = 60 * 60, // one hour is plenty
+ .sec_tag = sec_tag,
+ .subject = NULL,
+ };
+ err = modem_jwt_generate(&jwt);
+ if (err) {
+ MEMFAULT_LOG_ERROR("Failed to generate JWT, error: %d", err);
+ }
+ return err;
+}
+
+static const uint8_t coap_auth_request_template[] = {
+ 0x48, 0x02, // CoAP POST
+ 0xFF, 0xFF, // Message ID
+ 0x90, 0x4A, 0x50, 0x61, 0xBF, 0x40, 0x33, 0x40, // Token
+ 0xB4, 0x61, 0x75, 0x74, 0x68, // URI: "auth"
+ 0x03, 0x6A, 0x77, 0x74, // URI "jwt"
+ 0x10, // Content-Type Text
+ 0xFF, // Payload-Marker
+};
+#define JWT_BUF_SZ 600
+#define COAP_AUTH_REQ_HEADER_SIZE (sizeof(coap_auth_request_template))
+#define COAP_AUTH_REQ_BUF_SIZE (JWT_BUF_SZ + COAP_AUTH_REQ_HEADER_SIZE)
+#define COAP_AUTH_REQ_MID_OFFSET 2
+#define COAP_AUTH_REQ_TOKEN_OFFSET 4
+#define COAP_REQ_WAIT_TIME_MS 300
+
+static int auth_socket(int socket) {
+ int32_t err = 0;
+ struct coap_packet reply;
+ uint8_t token[COAP_TOKEN_MAX_LEN];
+ uint8_t *packet_buf = k_malloc(COAP_AUTH_REQ_BUF_SIZE);
+ uint8_t response_token[COAP_TOKEN_MAX_LEN];
+ uint16_t mid = sys_cpu_to_be16(coap_next_id());
+ size_t request_size;
+
+ if (!packet_buf) {
+ MEMFAULT_LOG_ERROR("Failed to allocate memory for CoAP auth request buffer");
+ err = -ENOMEM;
+ goto end;
+ }
+
+ // Make new random token
+ sys_rand_get(token, COAP_TOKEN_MAX_LEN);
+
+ // Fill in CoAP header
+ memcpy(packet_buf, coap_auth_request_template, COAP_AUTH_REQ_HEADER_SIZE);
+ memcpy(packet_buf + COAP_AUTH_REQ_MID_OFFSET, &mid, sizeof(mid));
+ memcpy(packet_buf + COAP_AUTH_REQ_TOKEN_OFFSET, token, COAP_TOKEN_MAX_LEN);
+
+ // Generate JWT
+ err = prv_generate_jwt((char *)packet_buf + COAP_AUTH_REQ_HEADER_SIZE, JWT_BUF_SZ,
+ CONFIG_MEMFAULT_NRF_CLOUD_SEC_TAG);
+ if (err) {
+ MEMFAULT_LOG_ERROR("Error generating JWT with modem: %d", err);
+ goto end;
+ }
+
+ request_size =
+ COAP_AUTH_REQ_HEADER_SIZE + strnlen((char *)packet_buf + COAP_AUTH_REQ_HEADER_SIZE, JWT_BUF_SZ);
+
+ // Send the request
+ MEMFAULT_LOG_DEBUG("Sending CoAP auth request, size %zu", request_size);
+ err = zsock_send(socket, packet_buf, request_size, 0);
+ if (err < 0) {
+ MEMFAULT_LOG_ERROR("Failed to send CoAP request, errno %d", errno);
+ goto end;
+ }
+
+ for (size_t i = 0; i < 10; ++i) {
+ k_sleep(K_MSEC(COAP_REQ_WAIT_TIME_MS));
+ // Poll for response
+ err = zsock_recv(socket, packet_buf, COAP_AUTH_REQ_BUF_SIZE, ZSOCK_MSG_DONTWAIT);
+ if (err < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ continue;
+ } else {
+ err = -errno;
+ MEMFAULT_LOG_ERROR("Error receiving response: %d", err);
+ goto end;
+ }
+ }
+ MEMFAULT_LOG_DEBUG("Received CoAP auth response, size %d", err);
+ // Parse response
+ err = coap_packet_parse(&reply, packet_buf, err, NULL, 0);
+ if (err < 0) {
+ MEMFAULT_LOG_ERROR("Error parsing response: %d", err);
+ continue;
+ }
+ // Match token
+ coap_header_get_token(&reply, response_token);
+ if (memcmp(response_token, token, COAP_TOKEN_MAX_LEN) != 0) {
+ continue;
+ }
+ // Check response code
+ if (coap_header_get_code(&reply) != COAP_RESPONSE_CODE_CREATED) {
+ MEMFAULT_LOG_ERROR("Error in response: %d", coap_header_get_code(&reply));
+ continue;
+ } else {
+ err = 0;
+ goto end;
+ }
+ }
+ err = -ETIMEDOUT;
+
+end:
+ k_free(packet_buf);
+ return err;
+}
+
+static int prv_getaddrinfo(struct zsock_addrinfo **res, const char *host, int port_num) {
+ struct zsock_addrinfo hints = {
+ .ai_family = AF_INET,
+ .ai_socktype = SOCK_DGRAM,
+ };
+
+ char port[10] = { 0 };
+ snprintf(port, sizeof(port), "%d", port_num);
+
+ int rv = zsock_getaddrinfo(host, port, &hints, res);
+ if (rv != 0) {
+ MEMFAULT_LOG_ERROR("DNS lookup for %s failed: %d", host, rv);
+ } else {
+ struct sockaddr_in *addr = net_sin((*res)->ai_addr);
+
+ MEMFAULT_LOG_DEBUG("DNS lookup for %s = %d.%d.%d.%d", host, addr->sin_addr.s4_addr[0],
+ addr->sin_addr.s4_addr[1], addr->sin_addr.s4_addr[2],
+ addr->sin_addr.s4_addr[3]);
+ }
+
+ return rv;
+}
+
+static int prv_create_socket(struct zsock_addrinfo **res, const char *host, int port_num) {
+ int rv = prv_getaddrinfo(res, host, port_num);
+ if (rv) {
+ MEMFAULT_LOG_ERROR("Failed to get address info, error: %d", rv);
+ return rv;
+ }
+
+ int fd = zsock_socket((*res)->ai_family, (*res)->ai_socktype, IPPROTO_DTLS_1_2);
+ if (fd < 0) {
+ MEMFAULT_LOG_ERROR("Failed to open socket, errno=%d", errno);
+ }
+
+ return fd;
+}
+
+static int prv_configure_dtls_socket(int sock_fd, const char *host) {
+ const int sec_tag_opt[] = { CONFIG_MEMFAULT_NRF_CLOUD_SEC_TAG };
+ int rv = zsock_setsockopt(sock_fd, SOL_TLS, TLS_SEC_TAG_LIST, sec_tag_opt, sizeof(sec_tag_opt));
+ int verify = TLS_PEER_VERIFY_REQUIRED;
+
+ if (rv) {
+ MEMFAULT_LOG_ERROR("Failed to setup sec tag, err %d", errno);
+ return rv;
+ }
+
+ rv = zsock_setsockopt(sock_fd, SOL_TLS, TLS_PEER_VERIFY, &verify, sizeof(verify));
+ if (rv) {
+ MEMFAULT_LOG_ERROR("Failed to setup peer verification, err %d", errno);
+ return rv;
+ }
+
+ uint32_t dtls_cid = TLS_DTLS_CID_ENABLED;
+
+ rv = zsock_setsockopt(sock_fd, SOL_TLS, TLS_DTLS_CID, &dtls_cid, sizeof(dtls_cid));
+ if (rv) {
+ MEMFAULT_LOG_ERROR("Failed to setup CID, err %d", errno);
+ return rv;
+ }
+
+ return zsock_setsockopt(sock_fd, SOL_TLS, TLS_HOSTNAME, host, strlen(host) + 1);
+}
+
+static int prv_configure_socket(int fd, const char *host) {
+ int rv = 0;
+
+ rv = prv_configure_dtls_socket(fd, host);
+ if (rv < 0) {
+ MEMFAULT_LOG_ERROR("Failed to configure tls w/ host, errno=%d", errno);
+ }
+
+ return rv;
+}
+
+static int prv_connect_socket(int fd, struct zsock_addrinfo *res) {
+ int rv = zsock_connect(fd, res->ai_addr, res->ai_addrlen);
+
+ if (rv < 0) {
+ MEMFAULT_LOG_ERROR("Failed to connect socket, errno=%d", errno);
+ zsock_close(fd);
+ }
+
+ return rv;
+}
+
+static int prv_open_socket(struct zsock_addrinfo **res, const char *host, int port_num) {
+ const int sock_fd = prv_create_socket(res, host, port_num);
+ if (sock_fd < 0) {
+ MEMFAULT_LOG_ERROR("Failed to create socket, errno=%d", sock_fd);
+ return sock_fd;
+ }
+
+ int rv = prv_configure_socket(sock_fd, host);
+ if (rv < 0) {
+ MEMFAULT_LOG_ERROR("Failed to configure socket, errno=%d", rv);
+ zsock_close(sock_fd);
+ return rv;
+ }
+
+ rv = prv_connect_socket(sock_fd, *res);
+ if (rv < 0) {
+ MEMFAULT_LOG_ERROR("Failed to connect socket, errno=%d", rv);
+ zsock_close(sock_fd);
+ return rv;
+ }
+
+ rv = auth_socket(sock_fd);
+ if (rv < 0) {
+ MEMFAULT_LOG_ERROR("Failed to auth socket, errno=%d", rv);
+ zsock_close(sock_fd);
+ return rv;
+ }
+ return sock_fd;
+}
+
+static int memfault_coap_make_header(sMemfaultCoAPContext *ctx, uint8_t *buf, size_t buf_len) {
+ const char *uri_path = "proxy";
+ const char *proxy_uri = "https://chunks.memfault.com/api/v0/chunks/%s";
+ sMemfaultDeviceInfo info = { 0 };
+
+ memfault_http_get_device_info(&info);
+
+ const size_t proxy_uri_max_len = strlen(proxy_uri) - 2 + strlen(info.device_serial) + 1;
+ uint8_t *proxy_uri_buf = k_malloc(proxy_uri_max_len);
+
+ if (!proxy_uri_buf) {
+ return -ENOMEM;
+ }
+
+ snprintf(proxy_uri_buf, proxy_uri_max_len, proxy_uri, info.device_serial);
+
+ int rv = 0;
+ struct coap_packet request;
+ memcpy(ctx->message_token, coap_next_token(), sizeof(ctx->message_token));
+
+ rv = coap_packet_init(&request, buf, buf_len, 1, COAP_TYPE_CON, COAP_TOKEN_MAX_LEN,
+ ctx->message_token, COAP_METHOD_POST, coap_next_id());
+ if (rv) {
+ goto exit;
+ }
+ rv = coap_packet_append_option(&request, COAP_OPTION_URI_PATH, uri_path, strlen(uri_path));
+ if (rv) {
+ goto exit;
+ }
+ rv = coap_packet_append_option(&request, COAP_OPTION_PROXY_URI, proxy_uri_buf,
+ strlen(proxy_uri_buf));
+ if (rv) {
+ goto exit;
+ }
+ rv = coap_packet_append_option(&request, MEMFAULT_NRF_CLOUD_COAP_PROJECT_KEY_OPTION_NO,
+ CONFIG_MEMFAULT_PROJECT_KEY, strlen(CONFIG_MEMFAULT_PROJECT_KEY));
+ if (rv) {
+ goto exit;
+ }
+ rv = coap_packet_append_payload_marker(&request);
+ if (rv) {
+ goto exit;
+ }
+
+ rv = request.offset;
+
+exit:
+ k_free(proxy_uri_buf);
+ return rv;
+}
+
+static int prv_poll_socket(int sock_fd, int events) {
+ struct zsock_pollfd poll_fd = {
+ .fd = sock_fd,
+ .events = events,
+ };
+ const int timeout_ms = CONFIG_MEMFAULT_COAP_CLIENT_TIMEOUT_MS;
+ int rv = zsock_poll(&poll_fd, 1, timeout_ms);
+ if (rv == 0) {
+ MEMFAULT_LOG_ERROR("Timeout waiting for socket event(s): event(s)=%d, errno=%d", events, errno);
+ }
+ return rv;
+}
+
+static bool prv_try_send(int sock_fd, const uint8_t *buf, size_t buf_len) {
+ size_t idx = 0;
+ while (idx != buf_len) {
+ // Wait for socket to become available within a timeout in case the socket is busy processing
+ // other tx data. This will prevent busy looping in this loop (since the send call is
+ // non-blocking), therefore allowing other threads to run.
+ int rv = prv_poll_socket(sock_fd, ZSOCK_POLLOUT);
+ if (rv <= 0) {
+ MEMFAULT_LOG_ERROR("Socket not ready for send: errno=%d", errno);
+ return false;
+ }
+
+ rv = zsock_send(sock_fd, &buf[idx], buf_len - idx, ZSOCK_MSG_DONTWAIT);
+ if (rv > 0) {
+ idx += rv;
+ continue;
+ }
+ if (rv <= 0) {
+ if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
+ continue;
+ }
+ MEMFAULT_LOG_ERROR("Data Send Error: len=%d, errno=%d", (int)buf_len, errno);
+ return false;
+ }
+ }
+ return true;
+}
+
+//! Returns:
+//! 0 - no more data to send
+//! 1 - data sent, awaiting response
+//! -1 - error
+static int prv_send_next_msg(sMemfaultCoAPContext *ctx) {
+ int sock = ctx->sock_fd;
+ uint8_t *buf = k_malloc(CONFIG_MEMFAULT_COAP_PACKETIZER_BUFFER_SIZE);
+ size_t buf_len = CONFIG_MEMFAULT_COAP_PACKETIZER_BUFFER_SIZE;
+ size_t chunk_size = 0;
+ int header_len = 0;
+ int rv = 0;
+
+ if (!buf) {
+ MEMFAULT_LOG_ERROR("Failed to allocate buffer for reading data");
+ return -1;
+ }
+
+ header_len = memfault_coap_make_header(ctx, buf, buf_len);
+ if (header_len < 0) {
+ MEMFAULT_LOG_ERROR("Failed to make coap header, err: %d", header_len);
+ rv = -1;
+ goto exit;
+ }
+ chunk_size = buf_len - header_len;
+
+ bool data_available = memfault_packetizer_get_chunk(buf + header_len, &chunk_size);
+ if (!data_available) {
+ MEMFAULT_LOG_DEBUG("No more data to send");
+ rv = 0;
+ goto exit;
+ }
+
+ buf_len = header_len + chunk_size;
+
+ MEMFAULT_LOG_DEBUG("Sending CoAP message, size %zu, fd %d", buf_len, sock);
+
+ if (!prv_try_send(sock, buf, buf_len)) {
+ MEMFAULT_LOG_ERROR("Failed to send CoAP request, errno %d", errno);
+ memfault_packetizer_abort();
+ rv = -1;
+ goto exit;
+ }
+
+ // count bytes sent
+ ctx->bytes_sent += buf_len;
+ rv = 1;
+
+ // message sent, await response
+exit:
+ k_free(buf);
+ return rv;
+}
+
+int memfault_zephyr_port_coap_open_socket(sMemfaultCoAPContext *ctx) {
+ const char *host = CONFIG_MEMFAULT_NRF_CLOUD_HOST_NAME;
+ const int port = MEMFAULT_NRF_CLOUD_COAP_PORT;
+
+ memfault_zephyr_port_coap_close_socket(ctx);
+
+ MEMFAULT_LOG_DEBUG("Opening socket to %s:%d", host, port);
+
+ ctx->sock_fd = prv_open_socket(&(ctx->res), host, port);
+
+ if (ctx->sock_fd < 0) {
+ MEMFAULT_LOG_ERROR("Failed to open socket: %d", ctx->sock_fd);
+ memfault_zephyr_port_coap_close_socket(ctx);
+ return -1;
+ }
+
+ MEMFAULT_LOG_DEBUG("Socket fd=%d", ctx->sock_fd);
+
+ return 0;
+}
+
+void memfault_zephyr_port_coap_close_socket(sMemfaultCoAPContext *ctx) {
+ if (ctx->sock_fd >= 0) {
+ zsock_close(ctx->sock_fd);
+ }
+ ctx->sock_fd = -1;
+
+ if (ctx->res != NULL) {
+ freeaddrinfo(ctx->res);
+ ctx->res = NULL;
+ }
+}
+
+static int prv_wait_for_coap_response(sMemfaultCoAPContext *ctx) {
+ int rv = -1;
+ uint8_t packet_buf[32]; // empirically 17 bytes
+ struct coap_packet reply;
+ uint8_t response_token[COAP_TOKEN_MAX_LEN];
+
+ while (true) {
+ rv = prv_poll_socket(ctx->sock_fd, ZSOCK_POLLIN);
+ if (rv <= 0) {
+ return -1;
+ }
+ rv = zsock_recv(ctx->sock_fd, packet_buf, sizeof(packet_buf), ZSOCK_MSG_DONTWAIT);
+ if (rv < 0) {
+ rv = -errno;
+ MEMFAULT_LOG_ERROR("Error receiving response: %d", rv);
+ return rv;
+ }
+ MEMFAULT_LOG_DEBUG("Received CoAP response, size %d", rv);
+ // Parse response
+ rv = coap_packet_parse(&reply, packet_buf, rv, NULL, 0);
+ if (rv < 0) {
+ MEMFAULT_LOG_ERROR("Error parsing response: %d", rv);
+ return rv;
+ }
+ // Match token
+ coap_header_get_token(&reply, response_token);
+ if (memcmp(response_token, ctx->message_token, COAP_TOKEN_MAX_LEN) != 0) {
+ continue;
+ }
+ // Check response code
+ if (coap_header_get_code(&reply) != COAP_RESPONSE_CODE_CREATED) {
+ MEMFAULT_LOG_ERROR("Unexpected response code: %d", coap_header_get_code(&reply));
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+}
+
+int memfault_zephyr_port_coap_upload_sdk_data(sMemfaultCoAPContext *ctx) {
+ int max_messages_to_send = CONFIG_MEMFAULT_HTTP_MAX_MESSAGES_TO_SEND;
+
+#if CONFIG_MEMFAULT_HTTP_MAX_POST_SIZE && CONFIG_MEMFAULT_RAM_BACKED_COREDUMP
+ // The largest data type we will send is a coredump. If CONFIG_MEMFAULT_HTTP_MAX_POST_SIZE
+ // is being used, make sure we issue enough HTTP POSTS such that an entire coredump will be sent.
+ max_messages_to_send =
+ MEMFAULT_MAX(max_messages_to_send,
+ CONFIG_MEMFAULT_RAM_BACKED_COREDUMP_SIZE / CONFIG_MEMFAULT_HTTP_MAX_POST_SIZE);
+#endif
+ bool success = true;
+ int rv = -1;
+
+ while (max_messages_to_send-- > 0) {
+ rv = prv_send_next_msg(ctx);
+ if (rv == 0) {
+ // no more messages to send
+ break;
+ } else if (rv < 0) {
+ success = false;
+ break;
+ }
+ success = !prv_wait_for_coap_response(ctx);
+ if (!success) {
+ break;
+ }
+ }
+
+ if ((max_messages_to_send <= 0) && memfault_packetizer_data_available()) {
+ MEMFAULT_LOG_WARN(
+ "Hit max message limit: " STRINGIFY(CONFIG_MEMFAULT_HTTP_MAX_MESSAGES_TO_SEND));
+ }
+
+ return success ? 0 : -1;
+}
+
+ssize_t memfault_zephyr_port_coap_post_data_return_size(void) {
+ if (!memfault_packetizer_data_available()) {
+ return 0;
+ }
+
+ sMemfaultCoAPContext ctx = { 0 };
+ ctx.sock_fd = -1;
+
+ int rv = memfault_zephyr_port_coap_open_socket(&ctx);
+ MEMFAULT_LOG_DEBUG("Opened CoAP socket, rv=%d", rv);
+
+ if (rv == 0) {
+ rv = memfault_zephyr_port_coap_upload_sdk_data(&ctx);
+ memfault_zephyr_port_coap_close_socket(&ctx);
+ }
+
+#if defined(CONFIG_MEMFAULT_METRICS_SYNC_SUCCESS)
+ if (rv == 0) {
+ memfault_metrics_connectivity_record_memfault_sync_success();
+ } else {
+ memfault_metrics_connectivity_record_memfault_sync_failure();
+ }
+#endif
+
+ return (rv == 0) ? (ctx.bytes_sent) : rv;
+}
diff --git a/ports/zephyr/ncs/src/memfault_platform_npm13xx_battery.c b/ports/zephyr/ncs/src/memfault_platform_npm13xx_battery.c
new file mode 100644
index 0000000..2dacb4f
--- /dev/null
+++ b/ports/zephyr/ncs/src/memfault_platform_npm13xx_battery.c
@@ -0,0 +1,152 @@
+//! @file
+//!
+//! Copyright (c) Memfault, Inc.
+//! See LICENSE for details
+//!
+
+#include MEMFAULT_ZEPHYR_INCLUDE(devicetree.h)
+#include MEMFAULT_ZEPHYR_INCLUDE(kernel.h)
+#include MEMFAULT_ZEPHYR_INCLUDE(drivers/sensor.h)
+#include MEMFAULT_ZEPHYR_INCLUDE(drivers/sensor/npm13xx_charger.h)
+#include MEMFAULT_ZEPHYR_INCLUDE(drivers/mfd/npm13xx.h)
+#include "memfault/components.h"
+#include "memfault_nrf_platform_battery_model.h"
+#include "nrf_fuel_gauge.h"
+
+// nPM13xx BCHGCHARGESTATUS register mask definitions, compatible with both nPM1300 and nPM1304
+// See https://docs-be.nordicsemi.com/bundle/ps_npm1300/page/nPM1300_PS_v1.2.pdf and
+// https://docs-be.nordicsemi.com/bundle/ps_npm1304/page/pdf/nPM1304_Preliminary_Datasheet_v0.7.pdf
+#define NPM13XX_CHG_STATUS_TC_MASK BIT(2)
+#define NPM13XX_CHG_STATUS_CC_MASK BIT(3)
+#define NPM13XX_CHG_STATUS_CV_MASK BIT(4)
+
+#if defined(CONFIG_DT_HAS_NORDIC_NPM1300_CHARGER_ENABLED)
+static const struct device *s_npm13xx_dev = DEVICE_DT_GET(DT_NODELABEL(npm1300_charger));
+#elif defined(CONFIG_DT_HAS_NORDIC_NPM1304_CHARGER_ENABLED)
+static const struct device *s_npm13xx_dev = DEVICE_DT_GET(DT_NODELABEL(npm1304_charger));
+#else
+ #error \
+ "Unsupported nPM13xx charger device or device not enabled in devicetree. Contact mflt.io/contact-support for assistance."
+#endif
+
+static int64_t s_ref_time;
+
+static int prv_npm13xx_read_sensors(float *voltage, float *current, float *temp,
+ int *charging_status) {
+ struct sensor_value reading;
+ int err;
+
+ err = sensor_sample_fetch(s_npm13xx_dev);
+ if (err < 0) {
+ return err;
+ }
+
+ err = sensor_channel_get(s_npm13xx_dev, SENSOR_CHAN_GAUGE_VOLTAGE, &reading);
+ if (err) {
+ return err;
+ }
+ *voltage = sensor_value_to_float(&reading);
+
+ err = sensor_channel_get(s_npm13xx_dev, SENSOR_CHAN_GAUGE_TEMP, &reading);
+ if (err) {
+ return err;
+ }
+ *temp = sensor_value_to_float(&reading);
+
+ err = sensor_channel_get(s_npm13xx_dev, SENSOR_CHAN_GAUGE_AVG_CURRENT, &reading);
+ if (err) {
+ return err;
+ }
+
+ // Zephyr sensor API returns current as negative for discharging, positive for charging
+ // but nRF fuel gauge library expects opposite. Flip here for uniformity
+ *current = -sensor_value_to_float(&reading);
+
+ // optionally read charging status
+ if (charging_status == NULL) {
+ return 0;
+ }
+ err = sensor_channel_get(s_npm13xx_dev, (enum sensor_channel)SENSOR_CHAN_NPM13XX_CHARGER_STATUS,
+ &reading);
+ if (err) {
+ return err;
+ }
+ *charging_status = reading.val1;
+
+ return 0;
+}
+
+static bool prv_npm13xx_is_discharging(int charging_status) {
+ return (charging_status & (NPM13XX_CHG_STATUS_TC_MASK | NPM13XX_CHG_STATUS_CC_MASK |
+ NPM13XX_CHG_STATUS_CV_MASK)) == 0;
+}
+
+int memfault_platform_get_stateofcharge(sMfltPlatformBatterySoc *soc) {
+ // Float type required to retain precision when passing units of volts, amps, and degrees C to the
+ // nRF fuel gauge API
+ float voltage, current, temp;
+ int charging_status;
+ int err = prv_npm13xx_read_sensors(&voltage, ¤t, &temp, &charging_status);
+ if (err < 0) {
+ MEMFAULT_LOG_ERROR("Failure reading charger sensors, error: %d", err);
+ return -1;
+ }
+
+ int64_t time_delta_ms = k_uptime_delta(&s_ref_time);
+ float time_delta_s = (float)time_delta_ms / 1000.0f;
+ struct nrf_fuel_gauge_state_info info;
+ nrf_fuel_gauge_process(voltage, current, temp, time_delta_s, &info);
+
+ // soc_raw is already 0-100, so scale only by scale value
+ soc->soc = (uint32_t)(info.soc_raw * (float)CONFIG_MEMFAULT_METRICS_BATTERY_SOC_PCT_SCALE_VALUE);
+ soc->discharging = prv_npm13xx_is_discharging(charging_status);
+
+ // Scale by scale value (must match definition in memfault_metrics_heartbeat_ncs_port_config.def)
+ uint32_t voltage_scaled = (uint32_t)(voltage * 1000.0f);
+ MEMFAULT_METRIC_SET_UNSIGNED(battery_voltage, voltage_scaled);
+
+ MEMFAULT_LOG_DEBUG("Battery SOC %u.%03u%%, discharging=%d, voltage=%d mv", soc->soc / 1000,
+ soc->soc % 1000, soc->discharging, (int)voltage_scaled);
+
+ s_ref_time = k_uptime_get();
+ return 0;
+}
+
+static int prv_platform_battery_init(void) {
+ if (!device_is_ready(s_npm13xx_dev)) {
+ printk("Charger device not ready\n");
+ return -1;
+ }
+
+ struct nrf_fuel_gauge_init_parameters parameters = { .model = &battery_model };
+ int err = prv_npm13xx_read_sensors(¶meters.v0, ¶meters.i0, ¶meters.t0, NULL);
+ if (err < 0) {
+ printk("Failure reading charger sensors, error: %d\n", err);
+ return err;
+ }
+
+ err = nrf_fuel_gauge_init(¶meters, NULL);
+ if (err) {
+ printk("Failure initializing fuel gauge, error: %d\n", err);
+ return err;
+ }
+
+ s_ref_time = k_uptime_get();
+ return 0;
+}
+
+#if defined(CONFIG_MEMFAULT_INIT_LEVEL_POST_KERNEL)
+ #error \
+ "CONFIG_MEMFAULT_INIT_LEVEL_POST_KERNEL=y not supported with CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX. Please contact mflt.io/contact-support for assistance."
+#elif (CONFIG_MEMFAULT_INIT_PRIORITY <= CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX_INIT_PRIORITY)
+ #error \
+ "MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX_INIT_PRIORITY must be set to a value less than MEMFAULT_INIT_PRIORITY (lower value = higher priority)"
+#endif
+
+SYS_INIT(prv_platform_battery_init,
+#if defined(CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX_INIT_LEVEL_POST_KERNEL)
+ POST_KERNEL,
+#else
+ APPLICATION,
+#endif
+ CONFIG_MEMFAULT_NRF_PLATFORM_BATTERY_NPM13XX_INIT_PRIORITY);
diff --git a/ports/zephyr/ncs/src/nrfx_pmu_reboot_tracking.c b/ports/zephyr/ncs/src/nrfx_pmu_reboot_tracking.c
index 27294f9..8f3eb94 100644
--- a/ports/zephyr/ncs/src/nrfx_pmu_reboot_tracking.c
+++ b/ports/zephyr/ncs/src/nrfx_pmu_reboot_tracking.c
@@ -86,6 +86,22 @@
return reset_reason;
}
#else
+
+// nRF Connect SDK v3.2.0 changed how these macros are defined. Check for the
+// presence of the new macros, falling back on the prior
+// 'NRF_RESET_HAS_NETWORK' workaround.
+ #if defined(NRF_RESET_HAS_LSREQ_RESET)
+ #define MEMFAULT_NRF_RESET_HAS_LSREQ_RESET NRF_RESET_HAS_LSREQ_RESET
+ #else
+ #define MEMFAULT_NRF_RESET_HAS_LSREQ_RESET NRF_RESET_HAS_NETWORK
+ #endif
+
+ #if defined(NRF_RESET_HAS_LCTRLAP_RESET)
+ #define MEMFAULT_NRF_RESET_HAS_LCTRLAP_RESET NRF_RESET_HAS_LCTRLAP_RESET
+ #else
+ #define MEMFAULT_NRF_RESET_HAS_LCTRLAP_RESET NRF_RESET_HAS_NETWORK
+ #endif
+
static eMemfaultRebootReason prv_decode_reset_resetreas(uint32_t reset_cause) {
eMemfaultRebootReason reset_reason;
if (reset_cause & NRF_RESET_RESETREAS_RESETPIN_MASK) {
@@ -121,7 +137,7 @@
} else if (reset_cause & NRF_RESET_RESETREAS_DIF_MASK) {
MEMFAULT_PRINT_RESET_INFO(" Debug Interface Wakeup");
reset_reason = kMfltRebootReason_DeepSleep;
- #if NRF_RESET_HAS_NETWORK
+ #if MEMFAULT_NRF_RESET_HAS_LSREQ_RESET
} else if (reset_cause & NRF_RESET_RESETREAS_LSREQ_MASK) {
MEMFAULT_PRINT_RESET_INFO(" Software (Network)");
reset_reason = kMfltRebootReason_SoftwareReset;
@@ -141,7 +157,7 @@
MEMFAULT_PRINT_RESET_INFO(" Force off (Network)");
reset_reason = kMfltRebootReason_SoftwareReset;
#endif
- #if NRF_RESET_HAS_NETWORK
+ #if MEMFAULT_NRF_RESET_HAS_LCTRLAP_RESET
} else if (reset_cause & NRF_RESET_RESETREAS_LCTRLAP_MASK) {
MEMFAULT_PRINT_RESET_INFO(" Debugger (Network)");
reset_reason = kMfltRebootReason_SoftwareReset;