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, &current, &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(&parameters.v0, &parameters.i0, &parameters.t0, NULL);
+  if (err < 0) {
+    printk("Failure reading charger sensors, error: %d\n", err);
+    return err;
+  }
+
+  err = nrf_fuel_gauge_init(&parameters, 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;