From 4f50b120d6f8454c01e9b9f5127a69ed4b80d991 Mon Sep 17 00:00:00 2001
From: "Zachary J. Fields" <zachary_fields@yahoo.com>
Date: Thu, 22 Aug 2024 20:40:33 -0500
Subject: [PATCH 1/2] chore: Broaden API usage in example sketch

---
 .../ConnectionHandlerDemo.ino                 | 81 ++++++++++++++++---
 .../ConnectionHandlerDemo/arduino_secrets.h   | 12 +--
 2 files changed, 77 insertions(+), 16 deletions(-)

diff --git a/examples/ConnectionHandlerDemo/ConnectionHandlerDemo.ino b/examples/ConnectionHandlerDemo/ConnectionHandlerDemo.ino
index 13ad117c..46a0c9d4 100644
--- a/examples/ConnectionHandlerDemo/ConnectionHandlerDemo.ino
+++ b/examples/ConnectionHandlerDemo/ConnectionHandlerDemo.ino
@@ -1,10 +1,11 @@
-/* SECRET_ fields are in arduino_secrets.h included above
- * if using a WiFi board (Arduino MKR1000, MKR WiFi 1010, Nano 33 IoT, UNO
+/* SECRET_ fields are in `arduino_secrets.h` (included below)
+ *
+ * If using a WiFi board (Arduino MKR1000, MKR WiFi 1010, Nano 33 IoT, UNO
  * WiFi Rev 2 or ESP8266/32), create a WiFiConnectionHandler object by adding
- * Network Name (SECRET_SSID) and password (SECRET_PASS) in the arduino_secrets.h
- * file (or Secrets tab in Create Web Editor).
+ * Network Name (SECRET_WIFI_SSID) and password (SECRET_WIFI_PASS) in the
+ * arduino_secrets.h file (or Secrets tab in Create Web Editor).
  *
- *    WiFiConnectionHandler conMan(SECRET_SSID, SECRET_PASS);
+ *    WiFiConnectionHandler conMan(SECRET_WIFI_SSID, SECRET_WIFI_PASS);
  *
  * If using a MKR GSM 1400 or other GSM boards supporting the same API you'll
  * need a GSMConnectionHandler object as follows
@@ -27,14 +28,21 @@
  *
  */
 
+#include <Arduino_ConnectionHandler.h>
+
 #include "arduino_secrets.h"
 
-#include <Arduino_ConnectionHandler.h>
+#define CONN_TOGGLE_MS 60000
+
+#if !(defined(BOARD_HAS_WIFI) || defined(BOARD_HAS_GSM) || defined(BOARD_HAS_LORA) || \
+      defined(BOARD_HAS_NB) || defined(BOARD_HAS_ETHERNET) || defined(BOARD_HAS_CATM1_NBIOT))
+  #error "Please check Arduino Connection Handler supported boards list: https://github.com/arduino-libraries/Arduino_ConnectionHandler/blob/master/README.md"
+#endif
 
 #if defined(BOARD_HAS_ETHERNET)
 EthernetConnectionHandler conMan(SECRET_IP, SECRET_DNS, SECRET_GATEWAY, SECRET_NETMASK);
 #elif defined(BOARD_HAS_WIFI)
-WiFiConnectionHandler conMan(SECRET_SSID, SECRET_PASS);
+WiFiConnectionHandler conMan(SECRET_WIFI_SSID, SECRET_WIFI_PASS);
 #elif defined(BOARD_HAS_GSM)
 GSMConnectionHandler conMan(SECRET_PIN, SECRET_APN, SECRET_GSM_USER, SECRET_GSM_PASS);
 #elif defined(BOARD_HAS_NB)
@@ -47,19 +55,73 @@ CatM1ConnectionHandler conMan(SECRET_PIN, SECRET_APN, SECRET_GSM_USER, SECRET_GS
 CellularConnectionHandler conMan(SECRET_PIN, SECRET_APN, SECRET_GSM_USER, SECRET_GSM_PASS);
 #endif
 
+bool attemptConnect = false;
+uint32_t lastConnToggleMs = 0;
+
 void setup() {
+  /* Initialize serial debug port and wait up to 5 seconds for port to open */
   Serial.begin(9600);
-  /* Give a few seconds for the Serial connection to be available */
-  delay(4000);
+  for(unsigned long const serialBeginTime = millis(); !Serial && (millis() - serialBeginTime <= 5000); ) { }
+
 #ifndef __AVR__
+  /* Set the debug message level:
+   * - DBG_ERROR: Only show error messages
+   * - DBG_WARNING: Show warning and error messages
+   * - DBG_INFO: Show info, warning, and error messages
+   * - DBG_DEBUG: Show debug, info, warning, and error messages
+   * - DBG_VERBOSE: Show all messages
+   */
   setDebugMessageLevel(DBG_INFO);
 #endif
+
+  /* Add callbacks to the ConnectionHandler object to get notified of network
+   * connection events. */
   conMan.addCallback(NetworkConnectionEvent::CONNECTED, onNetworkConnect);
   conMan.addCallback(NetworkConnectionEvent::DISCONNECTED, onNetworkDisconnect);
   conMan.addCallback(NetworkConnectionEvent::ERROR, onNetworkError);
+
+  Serial.print("Network Adapter Interface: ");
+  switch (conMan.getInterface()) {
+    case NetworkAdapter::WIFI:
+      Serial.println("Wi-Fi");
+      break;
+    case NetworkAdapter::ETHERNET:
+      Serial.println("Ethernet");
+      break;
+    case NetworkAdapter::NB:
+      Serial.println("Narrowband");
+      break;
+    case NetworkAdapter::GSM:
+      Serial.println("GSM");
+      break;
+    case NetworkAdapter::LORA:
+      Serial.println("LoRa");
+      break;
+    case NetworkAdapter::CATM1:
+      Serial.println("Category M1");
+      break;
+    case NetworkAdapter::CELL:
+      Serial.println("Cellular");
+      break;
+    default:
+      Serial.println("Unknown");
+      break;
+  }
 }
 
 void loop() {
+  /* Toggle the connection every `CONN_TOGGLE_MS` milliseconds */
+  if ((millis() - lastConnToggleMs) > CONN_TOGGLE_MS) {
+    Serial.println("Toggling connection...");
+    if (attemptConnect) {
+      conMan.connect();
+    } else {
+      conMan.disconnect();
+    }
+    attemptConnect = !attemptConnect;
+    lastConnToggleMs = millis();
+  }
+
   /* The following code keeps on running connection workflows on our
    * ConnectionHandler object, hence allowing reconnection in case of failure
    * and notification of connect/disconnect event if enabled (see
@@ -68,7 +130,6 @@ void loop() {
    * which might not guarantee the correct functioning of the ConnectionHandler
    * object.
    */
-
   conMan.check();
 }
 
diff --git a/examples/ConnectionHandlerDemo/arduino_secrets.h b/examples/ConnectionHandlerDemo/arduino_secrets.h
index 4d9fb7c8..f9906f69 100644
--- a/examples/ConnectionHandlerDemo/arduino_secrets.h
+++ b/examples/ConnectionHandlerDemo/arduino_secrets.h
@@ -1,12 +1,12 @@
 // Required for WiFiConnectionHandler
-const char SECRET_SSID[] = "NETWORK NAME";
-const char SECRET_PASS[] = "NETWORK PASSWORD";
+const char SECRET_WIFI_SSID[] = "NETWORK NAME";
+const char SECRET_WIFI_PASS[] = "NETWORK PASSWORD";
 
 // Required for GSMConnectionHandler
-const char SECRET_APN[] 		= "MOBILE PROVIDER APN ADDRESS";
-const char SECRET_PIN[] 		= "0000";  // Required for NBConnectionHandler
-const char SECRET_GSM_USER[] 	= "GSM USERNAME";
-const char SECRET_GSM_PASS[]	= "GSM PASSWORD";
+const char SECRET_APN[]       = "MOBILE PROVIDER APN ADDRESS";
+const char SECRET_PIN[]       = "0000";  // Required for NBConnectionHandler
+const char SECRET_GSM_USER[]  = "GSM USERNAME";
+const char SECRET_GSM_PASS[]  = "GSM PASSWORD";
 
 // Required for LoRaConnectionHandler
 const char SECRET_APP_EUI[]  = "APP_EUI";

From df5c8271b34cd0ab22f6c457c5fc3bcad4ba85ce Mon Sep 17 00:00:00 2001
From: "Zachary J. Fields" <zachary_fields@yahoo.com>
Date: Thu, 22 Aug 2024 20:44:43 -0500
Subject: [PATCH 2/2] feat: NotecardConnectionHandler

---
 .github/workflows/compile-examples.yml        |  42 +
 README.md                                     |   8 +-
 .../ConnectionHandlerDemo-Notecard.ino        | 144 +++
 .../ConnectionHandlerDemo-Notecard/README.md  |  71 ++
 .../arduino_secrets.h                         |   5 +
 keywords.txt                                  |  10 +-
 library.properties                            |   6 +-
 src/Arduino_ConnectionHandler.h               |   6 +
 src/ConnectionHandlerDefinitions.h            |  15 +-
 src/ConnectionHandlerInterface.h              |  14 +-
 src/NotecardConnectionHandler.cpp             | 828 ++++++++++++++++++
 src/NotecardConnectionHandler.h               | 341 ++++++++
 12 files changed, 1475 insertions(+), 15 deletions(-)
 create mode 100644 examples/ConnectionHandlerDemo-Notecard/ConnectionHandlerDemo-Notecard.ino
 create mode 100644 examples/ConnectionHandlerDemo-Notecard/README.md
 create mode 100644 examples/ConnectionHandlerDemo-Notecard/arduino_secrets.h
 create mode 100644 src/NotecardConnectionHandler.cpp
 create mode 100644 src/NotecardConnectionHandler.h

diff --git a/.github/workflows/compile-examples.yml b/.github/workflows/compile-examples.yml
index 878d5669..c0c5b9a9 100644
--- a/.github/workflows/compile-examples.yml
+++ b/.github/workflows/compile-examples.yml
@@ -35,6 +35,9 @@ jobs:
         - name: MKRNB
         - name: MKRWAN
         - name: Arduino_Cellular
+        - name: Blues Wireless Notecard
+      SKETCH_PATHS: |
+        - examples/ConnectionHandlerDemo
       ARDUINOCORE_MBED_STAGING_PATH: extras/ArduinoCore-mbed
       ARDUINOCORE_API_STAGING_PATH: extras/ArduinoCore-API
       SKETCHES_REPORTS_PATH: sketches-reports
@@ -106,6 +109,8 @@ jobs:
             platforms: |
               # Install Arduino SAMD Boards via Boards Manager
               - name: arduino:samd
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
           - board:
               platform-name: arduino:mbed
             platforms: |
@@ -114,21 +119,53 @@ jobs:
               # Overwrite the Arduino mbed-Enabled Boards release version with version from the tip of the default branch (located in local path because of the need to first install ArduinoCore-API)
               - source-path: extras/ArduinoCore-mbed
                 name: arduino:mbed
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
+          - board:
+              platform-name: arduino:mbed_portenta
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
+          - board:
+              platform-name: arduino:mbed_nano
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
+          - board:
+              platform-name: arduino:mbed_nicla
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
+          - board:
+              platform-name: arduino:mbed_opta
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
+          - board:
+              platform-name: arduino:mbed_giga
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
+          - board:
+              platform-name: arduino:mbed_edge
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
           - board:
               platform-name: arduino:renesas_portenta
             platforms: |
               # Install Arduino Renesas portenta Boards via Boards Manager
               - name: arduino:renesas_portenta
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
           - board:
               platform-name: arduino:renesas_uno
             platforms: |
               # Install Arduino Renesas uno Boards via Boards Manager
               - name: arduino:renesas_uno
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
           - board:
               platform-name: arduino:esp32
             platforms: |
               # Install Arduino ESP32 Boards via Boards Manager
               - name: arduino:esp32
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
           - board:
               platform-name: esp8266:esp8266
             platforms: |
@@ -142,6 +179,8 @@ jobs:
               # Install ESP32 platform via Boards Manager
               - name: esp32:esp32
                 source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
+            sketch-paths: |
+              - examples/ConnectionHandlerDemo-Notecard
 
     steps:
       - uses: actions/checkout@v4
@@ -180,6 +219,9 @@ jobs:
           platforms: ${{ matrix.platforms }}
           fqbn: ${{ matrix.board.fqbn }}
           libraries: ${{ env.LIBRARIES }}
+          sketch-paths: |
+            ${{ env.SKETCH_PATHS }}
+            ${{ matrix.sketch-paths }}
           enable-deltas-report: 'true'
           sketches-report-path: ${{ env.SKETCHES_REPORTS_PATH }}
 
diff --git a/README.md b/README.md
index c2759ac3..139db7f9 100644
--- a/README.md
+++ b/README.md
@@ -6,21 +6,25 @@ Arduino Library for network connections management
 [![Spell Check status](https://github.com/arduino-libraries/Arduino_ConnectionHandler/actions/workflows/spell-check.yml/badge.svg)](https://github.com/arduino-libraries/Arduino_ConnectionHandler/actions/workflows/spell-check.yml)
 
 Library for handling and managing network connections by providing keep-alive functionality and automatic reconnection in case of connection-loss. It supports the following boards:
+
 * **WiFi**: [`MKR 1000`](https://store.arduino.cc/arduino-mkr1000-wifi), [`MKR WiFi 1010`](https://store.arduino.cc/arduino-mkr-wifi-1010), [`Nano 33 IoT`](https://store.arduino.cc/arduino-nano-33-iot), [`Portenta H7`](https://store.arduino.cc/products/portenta-h7), [`Nano RP2040 Connect`](https://store.arduino.cc/products/arduino-nano-rp2040-connect), [`Nicla Vision`](https://store.arduino.cc/products/nicla-vision), [`OPTA WiFi`](https://store.arduino.cc/products/opta-wifi), [`GIGA R1 WiFi`](https://store.arduino.cc/products/giga-r1-wifi), [`Portenta C33`](https://store.arduino.cc/products/portenta-c33), [`UNO R4 WiFi`](https://store.arduino.cc/products/uno-r4-wifi), [`Nano ESP32`](https://store.arduino.cc/products/nano-esp32), [`ESP8266`](https://github.com/esp8266/Arduino/releases/tag/2.5.0), [`ESP32`](https://github.com/espressif/arduino-esp32)
 * **GSM**: [`MKR GSM 1400`](https://store.arduino.cc/arduino-mkr-gsm-1400-1415)
 * **5G**: [`MKR NB 1500`](https://store.arduino.cc/arduino-mkr-nb-1500-1413)
 * **LoRa**: [`MKR WAN 1300/1310`](https://store.arduino.cc/mkr-wan-1310)
 * **Ethernet**: [`Portenta H7`](https://store.arduino.cc/products/portenta-h7) + [`Vision Shield Ethernet`](https://store.arduino.cc/products/arduino-portenta-vision-shield-ethernet), [`Max Carrier`](https://store.arduino.cc/products/portenta-max-carrier), [`Breakout`](https://store.arduino.cc/products/arduino-portenta-breakout), [`Portenta Machine Control`](https://store.arduino.cc/products/arduino-portenta-machine-control), [`OPTA WiFi`](https://store.arduino.cc/products/opta-wifi), [`OPTA RS485`](https://store.arduino.cc/products/opta-rs485), [`OPTA Lite`](https://store.arduino.cc/products/opta-lite), [`Portenta C33`](https://store.arduino.cc/products/portenta-c33) + [`Vision Shield Ethernet`](https://store.arduino.cc/products/arduino-portenta-vision-shield-ethernet)
+* **Notecard**: [Provides Cellular/LoRa/Satellite/Wi-Fi to any modern board/architecture](examples/ConnectionHandlerDemo-Notecard/README.md)
 
 ### How-to-use
 
 ```C++
 #include <Arduino_ConnectionHandler.h>
 /* ... */
-#if defined(BOARD_HAS_ETHERNET)
+#if defined(BOARD_HAS_NOTECARD)
+NotecardConnectionHandler conMan("com.domain.you:product");
+#elif defined(BOARD_HAS_ETHERNET)
 EthernetConnectionHandler conMan;
 #elif defined(BOARD_HAS_WIFI)
-WiFiConnectionHandler conMan("SECRET_SSID", "SECRET_PASS");
+WiFiConnectionHandler conMan("SECRET_WIFI_SSID", "SECRET_WIFI_PASS");
 #elif defined(BOARD_HAS_GSM)
 GSMConnectionHandler conMan("SECRET_PIN", "SECRET_APN", "SECRET_GSM_LOGIN", "SECRET_GSM_PASS");
 #elif defined(BOARD_HAS_NB)
diff --git a/examples/ConnectionHandlerDemo-Notecard/ConnectionHandlerDemo-Notecard.ino b/examples/ConnectionHandlerDemo-Notecard/ConnectionHandlerDemo-Notecard.ino
new file mode 100644
index 00000000..e4183618
--- /dev/null
+++ b/examples/ConnectionHandlerDemo-Notecard/ConnectionHandlerDemo-Notecard.ino
@@ -0,0 +1,144 @@
+/* SECRET_ fields are in `arduino_secrets.h` (included below)
+ *
+ * If using a Host + Notecard connected over I2C you'll need a
+ * NotecardConnectionHandler object as follows:
+ *
+ *   NotecardConnectionHandler conMan(NOTECARD_PRODUCT_UID);
+ *
+ * If using a Host + Notecard connected over Serial you'll need a
+ * NotecardConnectionHandler object as follows:
+ *
+ *   NotecardConnectionHandler conMan(NOTECARD_PRODUCT_UID, UART_INTERFACE);
+ */
+
+#include <Notecard.h> // MUST include this first to enable Notecard support
+#include <Arduino_ConnectionHandler.h>
+
+#include "arduino_secrets.h"
+
+/* Uncomment the following line to use this example in a manner that is more
+ * compatible with LoRa.
+ */
+// #define USE_NOTE_LORA
+
+#ifndef USE_NOTE_LORA
+#define CONN_TOGGLE_MS 60000
+#else
+#define CONN_TOGGLE_MS 300000
+#endif
+
+/* The Notecard can provide connectivity to almost any board via ESLOV (I2C)
+ * or UART. An empty string (or the default value provided below) will not
+ * override the Notecard's existing configuration.
+ * Learn more at: https://dev.blues.io */
+#define NOTECARD_PRODUCT_UID "com.domain.you:product"
+
+/* Uncomment the following line to use the Notecard over UART */
+// #define UART_INTERFACE Serial1
+
+#ifndef UART_INTERFACE
+NotecardConnectionHandler conMan(NOTECARD_PRODUCT_UID);
+#else
+NotecardConnectionHandler conMan(NOTECARD_PRODUCT_UID, UART_INTERFACE);
+#endif
+
+bool attemptConnect = false;
+uint32_t lastConnToggleMs = 0;
+
+void setup() {
+  /* Initialize serial debug port and wait up to 5 seconds for port to open */
+  Serial.begin(9600);
+  for(unsigned long const serialBeginTime = millis(); !Serial && (millis() - serialBeginTime <= 5000); ) { }
+
+  /* Set the debug message level:
+   * - DBG_ERROR: Only show error messages
+   * - DBG_WARNING: Show warning and error messages
+   * - DBG_INFO: Show info, warning, and error messages
+   * - DBG_DEBUG: Show debug, info, warning, and error messages
+   * - DBG_VERBOSE: Show all messages
+   */
+  setDebugMessageLevel(DBG_INFO);
+
+  /* Add callbacks to the ConnectionHandler object to get notified of network
+   * connection events. */
+  conMan.addCallback(NetworkConnectionEvent::CONNECTED, onNetworkConnect);
+  conMan.addCallback(NetworkConnectionEvent::DISCONNECTED, onNetworkDisconnect);
+  conMan.addCallback(NetworkConnectionEvent::ERROR, onNetworkError);
+
+  /* First call to `check()` initializes the connection to the Notecard. While
+   * not strictly necessary, it cleans up the logging from this application.
+   */
+  conMan.check();
+
+#ifndef USE_NOTE_LORA
+  /* Set the Wi-Fi credentials for the Notecard */
+  String ssid = SECRET_WIFI_SSID;
+  if (ssid.length() > 0 && ssid != "NETWORK NAME") {
+    conMan.setWiFiCredentials(SECRET_WIFI_SSID, SECRET_WIFI_PASS);
+  }
+#else
+  conMan.setNotehubPollingInterval(720);  // poll twice per day
+#endif
+
+  /* Confirm Interface */
+  Serial.print("Network Adapter Interface: ");
+  if (NetworkAdapter::NOTECARD == conMan.getInterface()) {
+    Serial.print("Notecard ");
+    Serial.print(conMan.getNotecardUid());
+#ifndef UART_INTERFACE
+    Serial.println(" (via I2C)");
+#else
+    Serial.println(" (via UART)");
+#endif
+  } else {
+    Serial.println("ERROR: Unexpected Interface");
+    while(1);
+  }
+
+  /* Display the Arduino IoT Cloud Device ID */
+  displayCachedDeviceId();
+}
+
+void loop() {
+  /* Toggle the connection every `CONN_TOGGLE_MS` milliseconds */
+  if ((millis() - lastConnToggleMs) > CONN_TOGGLE_MS) {
+    Serial.println("Toggling connection...");
+    if (attemptConnect) {
+      displayCachedDeviceId();
+      conMan.connect();
+    } else {
+      // Flush any queued Notecard requests before disconnecting
+      conMan.initiateNotehubSync(NotecardConnectionHandler::SyncType::Outbound);
+      conMan.disconnect();
+    }
+    attemptConnect = !attemptConnect;
+    lastConnToggleMs = millis();
+  }
+
+  /* The following code keeps on running connection workflows on our
+   * ConnectionHandler object, hence allowing reconnection in case of failure
+   * and notification of connect/disconnect event if enabled (see
+   * addConnectCallback/addDisconnectCallback) NOTE: any use of delay() within
+   * the loop or methods called from it will delay the execution of .update(),
+   * which might not guarantee the correct functioning of the ConnectionHandler
+   * object.
+   */
+  conMan.check();
+}
+
+void displayCachedDeviceId() {
+  Serial.print("Cached Arduino IoT Cloud Device ID: ");
+  Serial.println(conMan.getDeviceId());
+}
+
+void onNetworkConnect() {
+  Serial.println(">>>> CONNECTED to network");
+}
+
+void onNetworkDisconnect() {
+  Serial.println(">>>> DISCONNECTED from network");
+}
+
+void onNetworkError() {
+  Serial.println(">>>> ERROR");
+}
diff --git a/examples/ConnectionHandlerDemo-Notecard/README.md b/examples/ConnectionHandlerDemo-Notecard/README.md
new file mode 100644
index 00000000..7f90a7cf
--- /dev/null
+++ b/examples/ConnectionHandlerDemo-Notecard/README.md
@@ -0,0 +1,71 @@
+Notecard Connectivity
+=====================
+
+The Notecard is a wireless, secure abstraction for device connectivity, that can
+be used to enable _ANY*_ device with I2C, or UART, to connect to the Arduino IoT
+Cloud via cellular, LoRa, satellite or Wi-Fi!
+
+As a result, your existing device architecture can now have first class support
+in the Arduino IoT Cloud, by using a Notecard as a secure communication channel.
+
+> \*_While any device with I2C/UART may use the Notecard, the Arduino IoT Cloud
+> library is not supported by the AVR toolchain. Therefore, devices based on the
+> AVR architecture cannot access the Arduino IoT Cloud via the Notecard._
+>
+> _However, any device (including AVR), may use the Notecard library to send data
+> to Notehub, then that data may be routed to any endpoint of your choosing. See the
+> [Notecard Routing Guide](https://dev.blues.io/guides-and-tutorials/routing-data-to-cloud)
+> for more information..._
+
+Wireless Connectivity Options
+-----------------------------
+
+- [Cellular](https://shop.blues.com/collections/notecard/products/notecard-cellular)
+- [Cellular + Wi-Fi](https://shop.blues.com/collections/notecard/products/notecard-cell-wifi)
+- [Wi-Fi](https://shop.blues.com/collections/notecard/products/wifi-notecard)
+- [LoRa](https://shop.blues.com/collections/notecard/products/notecard-lora)
+- [Satellite](https://shop.blues.com/products/starnote)
+
+How it Works
+------------
+
+**Architecture Diagram:**
+
+```
+--------                ------------                  -----------           -----------
+|      |                |          |                  |         |           |         |
+| Host |                |          |      Secure      |         |           | Arduino |
+| MCU  |---<I2C/UART>---| Notecard | ( ( Wireless ) ) | Notehub |---<TLS>---|   IoT   |
+|      |                |          |     Protocol     |         |           |  Cloud  |
+|______|                |__________|                  |_________|           |_________|
+```
+
+Getting Started
+---------------
+
+### Setup a Notehub Account
+
+Using the Notecard only requires a couple of easy steps:
+
+1. [Purchase a Notecard](https://shop.blues.com/collections/notecard) (and
+[Notecarrier](https://shop.blues.com/collections/notecarrier)) that fits the
+needs of your device.
+   > _**NOTE:** We recommend starting with our [Dev Kit](https://shop.blues.com/products/blues-global-starter-kit)
+   > if you are unsure._
+1. [Setup a Notehub account](https://dev.blues.io/quickstart/notecard-quickstart/notecard-and-notecarrier-f/#set-up-notehub).
+   > _**NOTE:** Notehub accounts are free (no credit card required)._
+1. [Create a project on your Notehub account](https://dev.blues.io/quickstart/notecard-quickstart/notecard-and-notecarrier-f/#create-a-notehub-project).
+1. In `ConnectionHandlerDemo-Notecard`, replace "com.domain.you:product" (from
+`NOTECARD_PRODUCT_UID`) with the ProductUID of your new Notehub project.
+
+### Power-up the Device
+
+1. [Connect the Notecard to your Host MCU](https://dev.blues.io/quickstart/notecard-quickstart/notecard-and-notecarrier-f/#connect-your-notecard-and-notecarrier)
+1. Flash the `ConnectionHanderDemo-Notecard` example sketch to your device. You
+should see the device reporting itself as online in your [Notehub Project](https://notehub.io).
+
+### More Information
+
+For more information about the Notecard and Notehub in general, please see our
+[Quickstart Guide](https://dev.blues.io/quickstart/) for a general overview of
+how the Notecard and Notehub are designed to work.
diff --git a/examples/ConnectionHandlerDemo-Notecard/arduino_secrets.h b/examples/ConnectionHandlerDemo-Notecard/arduino_secrets.h
new file mode 100644
index 00000000..bd2a9d58
--- /dev/null
+++ b/examples/ConnectionHandlerDemo-Notecard/arduino_secrets.h
@@ -0,0 +1,5 @@
+/* If provided, the Wi-Fi Credentials will be passed along to the Notecard. If
+ * the Notecard supports Wi-Fi, it will attempt to connect to the network using
+ * these credentials, if not, the Notecard will safely ignore these values. */
+const char SECRET_WIFI_SSID[] = "NETWORK NAME";
+const char SECRET_WIFI_PASS[] = "NETWORK PASSWORD";
diff --git a/keywords.txt b/keywords.txt
index ef5227f0..68e2be2d 100644
--- a/keywords.txt
+++ b/keywords.txt
@@ -11,13 +11,17 @@ GSMConnectionHandler	KEYWORD1
 NBConnectionHandler	KEYWORD1
 LoRaConnectionHandler	KEYWORD1
 EthernetConnectionHandler	KEYWORD1
-CatM1ConnectionHandler KEYWORD1
+CatM1ConnectionHandler	KEYWORD1
+NotecardConnectionHandler	KEYWORD1
 
 ####################################################
 # Methods and Functions (KEYWORD2)
 ####################################################
 
 ConnectionHandler	KEYWORD2
+available	KEYWORD2
+read	KEYWORD2
+write	KEYWORD2
 check	KEYWORD2
 connect	KEYWORD2
 disconnect	KEYWORD2
@@ -26,6 +30,10 @@ getTime	KEYWORD2
 getClient	KEYWORD2
 getUDP	KEYWORD2
 
+# NotecardConnectionHandler.h
+initiateNotehubSync	KEYWORD2
+setWiFiCredentials	KEYWORD2
+
 ####################################################
 # Constants (LITERAL1)
 ####################################################
diff --git a/library.properties b/library.properties
index 16239e46..c2ebe2a0 100644
--- a/library.properties
+++ b/library.properties
@@ -2,9 +2,9 @@ name=Arduino_ConnectionHandler
 version=0.9.0
 author=Ubi de Feo, Cristian Maglie, Andrea Catozzi, Alexander Entinger et al.
 maintainer=Arduino <info@arduino.cc>
-sentence=Arduino Library for network connection management (WiFi, GSM, NB, [Ethernet])
+sentence=Arduino Library for network connection management (WiFi, GSM, NB, [Ethernet], Notecard)
 paragraph=Originally part of ArduinoIoTCloud
 category=Communication
 url=https://github.com/arduino-libraries/Arduino_ConnectionHandler
-architectures=samd,esp32,esp8266,mbed,megaavr,mbed_nano,mbed_portenta,mbed_nicla,mbed_opta,mbed_giga,renesas_portenta,renesas_uno,mbed_edge
-depends=Arduino_DebugUtils, WiFi101, WiFiNINA, MKRGSM, MKRNB, MKRWAN
+architectures=samd,esp32,esp8266,mbed,megaavr,mbed_nano,mbed_portenta,mbed_nicla,mbed_opta,mbed_giga,renesas_portenta,renesas_uno,mbed_edge,stm32
+depends=Arduino_DebugUtils, WiFi101, WiFiNINA, MKRGSM, MKRNB, MKRWAN, Blues Wireless Notecard (>=1.6.3)
diff --git a/src/Arduino_ConnectionHandler.h b/src/Arduino_ConnectionHandler.h
index bb4c0e64..48327268 100644
--- a/src/Arduino_ConnectionHandler.h
+++ b/src/Arduino_ConnectionHandler.h
@@ -29,6 +29,10 @@
 #include <Arduino.h>
 #include "ConnectionHandlerDefinitions.h"
 
+#if defined(BOARD_HAS_NOTECARD)
+  #include "NotecardConnectionHandler.h"
+#else
+
 #if defined(BOARD_HAS_WIFI)
   #include "WiFiConnectionHandler.h"
 #endif
@@ -57,4 +61,6 @@
   #include "CellularConnectionHandler.h"
 #endif
 
+#endif // BOARD_HAS_NOTECARD
+
 #endif /* CONNECTION_HANDLER_H_ */
diff --git a/src/ConnectionHandlerDefinitions.h b/src/ConnectionHandlerDefinitions.h
index 3fdde169..96bcd654 100644
--- a/src/ConnectionHandlerDefinitions.h
+++ b/src/ConnectionHandlerDefinitions.h
@@ -21,8 +21,16 @@
    INCLUDES
  ******************************************************************************/
 
+#if defined __has_include
+  #if __has_include (<Notecard.h>)
+    #define BOARD_HAS_NOTECARD
+  #endif
+#endif
+
 #include <Arduino.h>
 
+#ifndef BOARD_HAS_NOTECARD
+
 #ifdef ARDUINO_SAMD_MKR1000
   #define BOARD_HAS_WIFI
   #define NETWORK_HARDWARE_ERROR WL_NO_SHIELD
@@ -136,6 +144,8 @@
   #define NETWORK_HARDWARE_ERROR
 #endif
 
+#endif // BOARD_HAS_NOTECARD
+
 /******************************************************************************
    TYPEDEFS
  ******************************************************************************/
@@ -163,7 +173,8 @@ enum class NetworkAdapter {
   GSM,
   LORA,
   CATM1,
-  CELL
+  CELL,
+  NOTECARD
 };
 
 /******************************************************************************
@@ -173,7 +184,7 @@ enum class NetworkAdapter {
 static unsigned int const CHECK_INTERVAL_TABLE[] =
 {
   /* INIT          */ 100,
-#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
+#if defined(BOARD_HAS_NOTECARD) || defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
   /* CONNECTING    */ 4000,
 #else
   /* CONNECTING    */ 500,
diff --git a/src/ConnectionHandlerInterface.h b/src/ConnectionHandlerInterface.h
index 94768ea8..228827e7 100644
--- a/src/ConnectionHandlerInterface.h
+++ b/src/ConnectionHandlerInterface.h
@@ -48,16 +48,17 @@ class ConnectionHandler {
 
     NetworkConnectionState check();
 
-    #if defined(BOARD_HAS_WIFI) || defined(BOARD_HAS_GSM) || defined(BOARD_HAS_NB) || defined(BOARD_HAS_ETHERNET) || defined(BOARD_HAS_CATM1_NBIOT)
+    #if not defined(BOARD_HAS_LORA)
       virtual unsigned long getTime() = 0;
-      virtual Client &getClient() = 0;
-      virtual UDP &getUDP() = 0;
     #endif
 
-    #if defined(BOARD_HAS_LORA)
-      virtual int write(const uint8_t *buf, size_t size) = 0;
-      virtual int read() = 0;
+    #if defined(BOARD_HAS_NOTECARD) || defined(BOARD_HAS_LORA)
       virtual bool available() = 0;
+      virtual int read() = 0;
+      virtual int write(const uint8_t *buf, size_t size) = 0;
+    #else
+      virtual Client &getClient() = 0;
+      virtual UDP &getUDP() = 0;
     #endif
 
     NetworkConnectionState getStatus() __attribute__((deprecated)) {
@@ -87,7 +88,6 @@ class ConnectionHandler {
     virtual NetworkConnectionState update_handleDisconnecting() = 0;
     virtual NetworkConnectionState update_handleDisconnected () = 0;
 
-
   private:
 
     unsigned long _lastConnectionTickTime;
diff --git a/src/NotecardConnectionHandler.cpp b/src/NotecardConnectionHandler.cpp
new file mode 100644
index 00000000..5205c4a8
--- /dev/null
+++ b/src/NotecardConnectionHandler.cpp
@@ -0,0 +1,828 @@
+/*
+  This file is part of the ArduinoIoTCloud library.
+
+  Copyright 2024 Blues (http://www.blues.com/)
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+*/
+
+/******************************************************************************
+   INCLUDE
+ ******************************************************************************/
+
+#include "ConnectionHandlerDefinitions.h"
+
+#if defined(BOARD_HAS_NOTECARD) // Only compile if the Notecard is present
+
+#include "NotecardConnectionHandler.h"
+
+#include <Arduino.h>
+#include <Arduino_DebugUtils.h>
+#include <Wire.h>
+
+/******************************************************************************
+   DEFINES
+ ******************************************************************************/
+
+#define NO_INBOUND_POLLING -1
+
+#define NOTEFILE_BASE_NAME "arduino_iot_cloud"
+
+// Notecard LoRa requires us to choose an arbitrary port between 1-99
+#define NOTEFILE_DATABASE_LORA_PORT 1
+#define NOTEFILE_INBOUND_LORA_PORT 2
+#define NOTEFILE_OUTBOUND_LORA_PORT 3
+
+// Note that we use "s" versions of the Notefile extensions to ensure that
+// traffic always happens on a secure transport
+#define NOTEFILE_SECURE_DATABASE NOTEFILE_BASE_NAME ".dbs"
+#define NOTEFILE_SECURE_INBOUND NOTEFILE_BASE_NAME ".qis"
+#define NOTEFILE_SECURE_OUTBOUND NOTEFILE_BASE_NAME ".qos"
+
+/******************************************************************************
+   STLINK DEBUG OUTPUT
+ ******************************************************************************/
+
+// Provide Notehub debug output via STLINK serial port when available
+#if defined(ARDUINO_SWAN_R5) || defined(ARDUINO_CYGNET)
+  #define STLINK_DEBUG
+  HardwareSerial stlinkSerial(PIN_VCP_RX, PIN_VCP_TX);
+#endif
+
+/******************************************************************************
+   TYPEDEF
+ ******************************************************************************/
+
+struct NotecardConnectionStatus
+{
+  NotecardConnectionStatus(void) : transport_connected(0), connected_to_notehub(0), notecard_error(0), host_error(0), reserved(0) { }
+  NotecardConnectionStatus(uint_fast8_t x) : transport_connected(x & 0x01), connected_to_notehub(x & 0x02), notecard_error(x & 0x04), host_error(x & 0x08), reserved(x & 0xF0) { }
+  NotecardConnectionStatus & operator=(uint_fast8_t x) {
+      transport_connected  = (x & 0x01);
+      connected_to_notehub = (x & 0x02);
+      notecard_error       = (x & 0x04);
+      host_error           = (x & 0x08);
+      reserved             = (x & 0xF0);
+      return *this;
+  }
+  operator uint_fast8_t () const {
+      return ((reserved << 4) | (host_error << 3) | (notecard_error << 2) | (connected_to_notehub << 1) | (transport_connected));
+  }
+
+  bool transport_connected  : 1;
+  bool connected_to_notehub : 1;
+  bool notecard_error       : 1;
+  bool host_error           : 1;
+  uint_fast8_t reserved     : 4;
+};
+static_assert(sizeof(NotecardConnectionStatus) == sizeof(uint_fast8_t));
+
+/******************************************************************************
+   CTOR/DTOR
+ ******************************************************************************/
+
+NotecardConnectionHandler::NotecardConnectionHandler(
+  const String & project_uid_,
+  uint32_t i2c_address_,
+  uint32_t i2c_max_,
+  TwoWire & wire_,
+  bool keep_alive_
+) :
+  ConnectionHandler{keep_alive_, NetworkAdapter::NOTECARD},
+  _notecard{},
+  _device_id{},
+  _notecard_uid{},
+  _project_uid(project_uid_),
+  _serial(nullptr),
+  _wire(&wire_),
+  _inbound_buffer(nullptr),
+  _conn_start_ms(0),
+  _i2c_address(i2c_address_),
+  _i2c_max(i2c_max_),
+  _inbound_buffer_index(0),
+  _inbound_buffer_size(0),
+  _inbound_polling_interval_min(NO_INBOUND_POLLING),
+  _uart_baud(0),
+  _en_hw_int(false),
+  _topic_type{TopicType::Invalid}
+{ }
+
+NotecardConnectionHandler::NotecardConnectionHandler(
+  const String & project_uid_,
+  HardwareSerial & serial_,
+  uint32_t baud_,
+  bool keep_alive_
+) :
+  ConnectionHandler{keep_alive_, NetworkAdapter::NOTECARD},
+  _notecard{},
+  _device_id{},
+  _notecard_uid{},
+  _project_uid(project_uid_),
+  _serial(&serial_),
+  _wire(nullptr),
+  _inbound_buffer(nullptr),
+  _conn_start_ms(0),
+  _i2c_address(0),
+  _i2c_max(0),
+  _inbound_buffer_index(0),
+  _inbound_buffer_size(0),
+  _inbound_polling_interval_min(NO_INBOUND_POLLING),
+  _uart_baud(baud_),
+  _en_hw_int(false),
+  _topic_type{TopicType::Invalid}
+{ }
+
+/******************************************************************************
+   PUBLIC MEMBER FUNCTIONS
+ ******************************************************************************/
+
+int NotecardConnectionHandler::initiateNotehubSync (SyncType type_) const
+{
+  int result;
+
+  Debug.print(DBG_DEBUG, F("NotecardConnectionHandler::%s initiating Notehub sync..."), __FUNCTION__);
+  if (J *req = _notecard.newRequest("hub.sync")) {
+    if (type_ == SyncType::Inbound) {
+      JAddBoolToObject(req, "in", true);
+    } else if (type_ == SyncType::Outbound) {
+      JAddBoolToObject(req, "out", true);
+    }
+    if (J *rsp = _notecard.requestAndResponse(req)) {
+      // Check the response for errors
+      if (NoteResponseError(rsp)) {
+        const char *err = JGetString(rsp, "err");
+        Debug.print(DBG_ERROR, F("%s"), err);
+        result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+      } else {
+        Debug.print(DBG_DEBUG, F("NotecardConnectionHandler::%s successfully initiated Notehub sync."), __FUNCTION__);
+        result = NotecardCommunicationError::NOTECARD_ERROR_NONE;
+      }
+      JDelete(rsp);
+    } else {
+      Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+      result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+    }
+  } else {
+    Debug.print(DBG_ERROR, "Failed to allocate request: hub.sync");
+    result = NotecardCommunicationError::HOST_ERROR_OUT_OF_MEMORY;
+  }
+
+  return result;
+}
+
+int NotecardConnectionHandler::setWiFiCredentials (const String & ssid_, const String & password_)
+{
+  int result;
+
+  // Validate the connection state is not in an initialization state
+  const NetworkConnectionState current_net_connection_state = check();
+  if (NetworkConnectionState::INIT == current_net_connection_state)
+  {
+    Debug.print(DBG_ERROR, F("Unable to set Wi-Fi credentials. Connection to Notecard uninitialized."));
+    result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+  } else if (J *req = _notecard.newRequest("card.wifi")) {
+    JAddStringToObject(req, "ssid", ssid_.c_str());
+    JAddStringToObject(req, "password", password_.c_str());
+    if (J *rsp = _notecard.requestAndResponse(req)) {
+      // Check the response for errors
+      if (NoteResponseError(rsp)) {
+        const char *err = JGetString(rsp, "err");
+        Debug.print(DBG_ERROR, F("%s"), err);
+        Debug.print(DBG_ERROR, F("Failed to set Wi-Fi credentials."));
+        result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+      } else {
+        Debug.print(DBG_INFO, F("Wi-Fi credentials updated. ssid: \"%s\" password: \"%s\"."), ssid_.c_str(), password_.length() ? "**********" : "");
+        result = NotecardCommunicationError::NOTECARD_ERROR_NONE;
+      }
+      JDelete(rsp);
+    } else {
+      Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+      result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+    }
+  } else {
+    Debug.print(DBG_ERROR, F("Failed to allocate request: wifi.set"));
+    result = NotecardCommunicationError::HOST_ERROR_OUT_OF_MEMORY;
+  }
+
+  return result;
+}
+
+/******************************************************************************
+   PUBLIC INTERFACE MEMBER FUNCTIONS
+ ******************************************************************************/
+
+bool NotecardConnectionHandler::available()
+{
+  bool buffered_data = (_inbound_buffer_index < _inbound_buffer_size);
+  bool flush_required = !buffered_data && _inbound_buffer_size;
+
+  // When the buffer is empty, look for a Note in the
+  // NOTEFILE_SECURE_INBOUND file to reload the buffer.
+  if (!buffered_data) {
+    // Reset the buffer
+    free(_inbound_buffer);
+    _inbound_buffer = nullptr;
+    _inbound_buffer_index = 0;
+    _inbound_buffer_size = 0;
+
+    // Do NOT attempt to buffer the next Note immediately after buffer
+    // exhaustion (a.k.a. flush required). Returning `false` between Notes,
+    // will break the read loop, force the CBOR buffer to be parsed, and the
+    // property containers to be updated.
+    if (!flush_required) {
+      // Reload the buffer
+      J *note = getNote(true);
+      if (note) {
+        if (J *body = JGetObject(note, "body")) {
+          _topic_type = static_cast<TopicType>(JGetInt(body, "topic"));
+          if (_topic_type == TopicType::Invalid) {
+            Debug.print(DBG_WARNING, F("Note does not contain a topic"));
+          } else {
+            buffered_data = JGetBinaryFromObject(note, "payload", &_inbound_buffer, &_inbound_buffer_size);
+            if (!buffered_data) {
+              Debug.print(DBG_WARNING, F("Note does not contain payload data"));
+            } else {
+              Debug.print(DBG_DEBUG, F("NotecardConnectionHandler::%s buffered payload with size: %d"), __FUNCTION__, _inbound_buffer_size);
+            }
+          }
+        } else {
+          _topic_type = TopicType::Invalid;
+        }
+        JDelete(note);
+      }
+    }
+  }
+
+  return buffered_data;
+}
+
+unsigned long NotecardConnectionHandler::getTime()
+{
+  unsigned long result;
+
+  if (J *rsp = _notecard.requestAndResponse(_notecard.newRequest("card.time"))) {
+    if (NoteResponseError(rsp)) {
+      const char *err = JGetString(rsp, "err");
+      Debug.print(DBG_ERROR, F("%s\n"), err);
+      result = 0;
+    } else {
+      result = JGetInt(rsp, "time");
+    }
+    JDelete(rsp);
+  } else {
+    result = 0;
+  }
+
+  return result;
+}
+
+int NotecardConnectionHandler::read()
+{
+  int result;
+
+  if (_inbound_buffer_index < _inbound_buffer_size) {
+    result = _inbound_buffer[_inbound_buffer_index++];
+  } else {
+    result = NotecardCommunicationError::NOTECARD_ERROR_NO_DATA_AVAILABLE;
+  }
+
+  return result;
+}
+
+int NotecardConnectionHandler::write(const uint8_t * buf_, size_t size_)
+{
+  int result;
+
+  // Validate the connection state is not uninitialized or in error state
+  const NetworkConnectionState current_net_connection_state = check();
+  if ((NetworkConnectionState::INIT == current_net_connection_state)
+  || (NetworkConnectionState::ERROR == current_net_connection_state))
+  {
+    Debug.print(DBG_ERROR, F("Unable to write message. Connection to Notecard uninitialized or in error state."));
+    result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+  } else if (J * req = _notecard.newRequest("note.add")) {
+    JAddStringToObject(req, "file", NOTEFILE_SECURE_OUTBOUND);
+    if (buf_) {
+      JAddBinaryToObject(req, "payload", buf_, size_);
+    }
+    // Queue the Note when `_keep_alive` is disabled or not connected to Notehub
+    if (_keep_alive && (NetworkConnectionState::CONNECTED == current_net_connection_state)) {
+      JAddBoolToObject(req, "live", true);
+      JAddBoolToObject(req, "sync", true);
+    }
+    if (J *body = JAddObjectToObject(req, "body")) {
+      JAddIntToObject(body, "topic", static_cast<int>(_topic_type));
+      J * rsp = _notecard.requestAndResponse(req);
+      if (NoteResponseError(rsp)) {
+        const char *err = JGetString(rsp, "err");
+        if (NoteErrorContains(err, "{hub-not-connected}")) {
+          // _current_net_connection_state = NetworkConnectionState::DISCONNECTED;
+        }
+        Debug.print(DBG_ERROR, F("%s\n"), err);
+        result = NotecardCommunicationError::NOTECARD_ERROR_GENERIC;
+      } else {
+        result = NotecardCommunicationError::NOTECARD_ERROR_NONE;
+        Debug.print(DBG_INFO, F("Message sent correctly!"));
+      }
+      JDelete(rsp);
+    } else {
+      JFree(req);
+      result = NotecardCommunicationError::HOST_ERROR_OUT_OF_MEMORY;
+    }
+  } else {
+    result = NotecardCommunicationError::HOST_ERROR_OUT_OF_MEMORY;
+  }
+
+  return result;
+}
+
+/******************************************************************************
+   PROTECTED STATE MACHINE FUNCTIONS
+ ******************************************************************************/
+
+NetworkConnectionState NotecardConnectionHandler::update_handleInit()
+{
+  NetworkConnectionState result = NetworkConnectionState::INIT;
+
+ // Configure Hardware
+///////////////////////
+
+#if defined(STLINK_DEBUG)
+  // Output Notecard logs to the STLINK serial port
+  stlinkSerial.end();  // necessary to handle multiple initializations (e.g. reconnections)
+  stlinkSerial.begin(115200);
+  const size_t usb_timeout_ms = 3000;
+  for (const size_t start_ms = millis(); !stlinkSerial && (millis() - start_ms) < usb_timeout_ms;);
+  _notecard.setDebugOutputStream(stlinkSerial);
+#endif
+
+  // Initialize the Notecard based on the configuration
+  if (_serial) {
+    _notecard.begin(*_serial, _uart_baud);
+  } else {
+    _notecard.begin(_i2c_address, _i2c_max, *_wire);
+  }
+
+   // Configure `note-c`
+  ///////////////////////
+
+  // Set the user agent
+  NoteSetUserAgent((char *) ("arduino-iot-cloud " NOTECARD_CONNECTION_HANDLER_VERSION));
+
+  // Configure the ATTN pin to be used as an interrupt to indicate when a Note
+  // is available to read. `getNote()` will only arm the interrupt if no old
+  // Notes are available. If `ATTN` remains unarmed, it signals the user
+  // application that outstanding Notes are queued and need to be processed.
+  if (J *note = getNote(false)) {
+    JDelete(note);
+  }
+
+   // Configure the Notecard
+  ///////////////////////////
+
+  // Set the project UID
+  if (NetworkConnectionState::INIT == result) {
+    if (configureConnection(true)) {
+      result = NetworkConnectionState::INIT;
+    } else {
+      result = NetworkConnectionState::ERROR;
+    }
+  }
+
+#if defined(ARDUINO_OPTA)
+  // The Opta Extension has an onboard Li-Ion capacitor, that can be utilized
+  // to monitor the power state of the device and automatically report loss of
+  // power to Notehub. The following command enables that detection by default
+  // for the Opta Wirelss Extension.
+  if (NetworkConnectionState::INIT == result) {
+    if (J *req = _notecard.newRequest("card.voltage")) {
+      JAddStringToObject(req, "mode", "lipo");
+      JAddBoolToObject(req, "alert", true);
+      JAddBoolToObject(req, "sync", true);
+      JAddBoolToObject(req, "usb", true);
+      if (J *rsp = _notecard.requestAndResponse(req)) {
+        // Check the response for errors
+        if (NoteResponseError(rsp)) {
+          const char *err = JGetString(rsp, "err");
+          Debug.print(DBG_ERROR, F("%s"), err);
+          result = NetworkConnectionState::ERROR;
+        } else {
+          result = NetworkConnectionState::INIT;
+        }
+        JDelete(rsp);
+      } else {
+        Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+        result = NetworkConnectionState::ERROR; // Assume the worst
+      }
+    } else {
+      Debug.print(DBG_ERROR, "Failed to allocate request: card.voltage");
+      result = NetworkConnectionState::ERROR; // Assume the worst
+    }
+  }
+#endif
+
+  // Set database template to support LoRa/Satellite Notecard
+  if (NetworkConnectionState::INIT == result) {
+    if (J *req = _notecard.newRequest("note.template")) {
+      JAddStringToObject(req, "file", NOTEFILE_SECURE_DATABASE);
+      JAddStringToObject(req, "format", "compact");               // Support LoRa/Satellite Notecards
+      JAddIntToObject(req, "port", NOTEFILE_DATABASE_LORA_PORT);  // Support LoRa/Satellite Notecards
+      if (J *body = JAddObjectToObject(req, "body")) {
+        JAddStringToObject(body, "text", TSTRINGV);
+        JAddNumberToObject(body, "value", TFLOAT64);
+        JAddBoolToObject(body, "flag", TBOOL);
+        if (J *rsp = _notecard.requestAndResponse(req)) {
+          // Check the response for errors
+          if (NoteResponseError(rsp)) {
+            const char *err = JGetString(rsp, "err");
+            Debug.print(DBG_ERROR, F("%s"), err);
+            result = NetworkConnectionState::ERROR;
+          } else {
+            result = NetworkConnectionState::INIT;
+          }
+          JDelete(rsp);
+        } else {
+          Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+          result = NetworkConnectionState::ERROR; // Assume the worst
+        }
+      } else {
+        Debug.print(DBG_ERROR, "Failed to allocate request: note.template:body");
+        JFree(req);
+        result = NetworkConnectionState::ERROR; // Assume the worst
+      }
+    } else {
+      Debug.print(DBG_ERROR, "Failed to allocate request: note.template");
+      result = NetworkConnectionState::ERROR; // Assume the worst
+    }
+  }
+
+  // Set inbound template to support LoRa/Satellite Notecard
+  if (NetworkConnectionState::INIT == result) {
+    if (J *req = _notecard.newRequest("note.template")) {
+      JAddStringToObject(req, "file", NOTEFILE_SECURE_INBOUND);
+      JAddStringToObject(req, "format", "compact");              // Support LoRa/Satellite Notecards
+      JAddIntToObject(req, "port", NOTEFILE_INBOUND_LORA_PORT);  // Support LoRa/Satellite Notecards
+      if (J *body = JAddObjectToObject(req, "body")) {
+        JAddIntToObject(body, "topic", TUINT8);
+        if (J *rsp = _notecard.requestAndResponse(req)) {
+          // Check the response for errors
+          if (NoteResponseError(rsp)) {
+            const char *err = JGetString(rsp, "err");
+            Debug.print(DBG_ERROR, F("%s"), err);
+            result = NetworkConnectionState::ERROR;
+          } else {
+            result = NetworkConnectionState::INIT;
+          }
+          JDelete(rsp);
+        } else {
+          Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+          result = NetworkConnectionState::ERROR; // Assume the worst
+        }
+      } else {
+        Debug.print(DBG_ERROR, "Failed to allocate request: note.template:body");
+        JFree(req);
+        result = NetworkConnectionState::ERROR; // Assume the worst
+      }
+    } else {
+      Debug.print(DBG_ERROR, "Failed to allocate request: note.template");
+      result = NetworkConnectionState::ERROR; // Assume the worst
+    }
+  }
+
+  // Set outbound template to remove payload size restrictions
+  if (NetworkConnectionState::INIT == result) {
+    if (J *req = _notecard.newRequest("note.template")) {
+      JAddStringToObject(req, "file", NOTEFILE_SECURE_OUTBOUND);
+      JAddStringToObject(req, "format", "compact");               // Support LoRa/Satellite Notecards
+      JAddIntToObject(req, "port", NOTEFILE_OUTBOUND_LORA_PORT);  // Support LoRa/Satellite Notecards
+      if (J *body = JAddObjectToObject(req, "body")) {
+        JAddIntToObject(body, "topic", TUINT8);
+        if (J *rsp = _notecard.requestAndResponse(req)) {
+          // Check the response for errors
+          if (NoteResponseError(rsp)) {
+            const char *err = JGetString(rsp, "err");
+            Debug.print(DBG_ERROR, F("%s"), err);
+            result = NetworkConnectionState::ERROR;
+          } else {
+            result = NetworkConnectionState::INIT;
+          }
+          JDelete(rsp);
+        } else {
+          Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+          result = NetworkConnectionState::ERROR; // Assume the worst
+        }
+      } else {
+        Debug.print(DBG_ERROR, "Failed to allocate request: note.template:body");
+        JFree(req);
+        result = NetworkConnectionState::ERROR; // Assume the worst
+      }
+    } else {
+      Debug.print(DBG_ERROR, "Failed to allocate request: note.template");
+      result = NetworkConnectionState::ERROR; // Assume the worst
+    }
+  }
+
+  // Get the device UID
+  if (NetworkConnectionState::INIT == result) {
+    if (!updateUidCache()) {
+      result = NetworkConnectionState::ERROR;
+    } else {
+      Debug.print(DBG_INFO, F("Notecard has been initialized."));
+      if (_keep_alive) {
+        _conn_start_ms = ::millis();
+        Debug.print(DBG_INFO, F("Starting network connection..."));
+        result = NetworkConnectionState::CONNECTING;
+      } else {
+        Debug.print(DBG_INFO, F("Network is disconnected."));
+        result = NetworkConnectionState::DISCONNECTED;
+      }
+    }
+  }
+
+  return result;
+}
+
+NetworkConnectionState NotecardConnectionHandler::update_handleConnecting()
+{
+  NetworkConnectionState result;
+
+  // Check the connection status
+  const NotecardConnectionStatus conn_status = connected();
+
+  // Update the connection state
+  if (!conn_status.connected_to_notehub) {
+    if ((::millis() - _conn_start_ms) > NOTEHUB_CONN_TIMEOUT_MS) {
+      Debug.print(DBG_ERROR, F("Timeout exceeded, connection to the network failed."));
+      Debug.print(DBG_INFO, F("Retrying in \"%d\" milliseconds"), CHECK_INTERVAL_TABLE[static_cast<unsigned int>(NetworkConnectionState::CONNECTING)]);
+      result = NetworkConnectionState::INIT;
+    } else {
+      // Continue awaiting the connection to Notehub
+      if (conn_status.transport_connected) {
+        Debug.print(DBG_INFO, F("Establishing connection to Notehub..."));
+      } else {
+        Debug.print(DBG_INFO, F("Connecting to the network..."));
+      }
+      result = NetworkConnectionState::CONNECTING;
+    }
+  } else {
+    Debug.print(DBG_INFO, F("Connected to Notehub!"));
+    result = NetworkConnectionState::CONNECTED;
+    if (initiateNotehubSync()) {
+      Debug.print(DBG_ERROR, F("Failed to initiate Notehub sync."));
+    }
+  }
+
+  return result;
+}
+
+NetworkConnectionState NotecardConnectionHandler::update_handleConnected()
+{
+  NetworkConnectionState result;
+
+  const NotecardConnectionStatus conn_status = connected();
+  if (!conn_status.connected_to_notehub) {
+    if (!conn_status.transport_connected) {
+      Debug.print(DBG_ERROR, F("Connection to the network lost."));
+    } else {
+      Debug.print(DBG_ERROR, F("Connection to Notehub lost."));
+    }
+    result = NetworkConnectionState::DISCONNECTED;
+  } else {
+    result = NetworkConnectionState::CONNECTED;
+  }
+
+  return result;
+}
+
+NetworkConnectionState NotecardConnectionHandler::update_handleDisconnecting()
+{
+  NetworkConnectionState result;
+
+  Debug.print(DBG_ERROR, F("Connection to the network lost."));
+  result = NetworkConnectionState::DISCONNECTED;
+
+  return result;
+}
+
+NetworkConnectionState NotecardConnectionHandler::update_handleDisconnected()
+{
+  NetworkConnectionState result;
+
+  if (_keep_alive)
+  {
+    Debug.print(DBG_ERROR, F("Attempting reconnection..."));
+    result = NetworkConnectionState::INIT;
+  }
+  else
+  {
+    if (configureConnection(false)) {
+      result = NetworkConnectionState::CLOSED;
+      Debug.print(DBG_INFO, F("Closing connection..."));
+    } else {
+      result = NetworkConnectionState::ERROR;
+      Debug.print(DBG_INFO, F("Error closing connection..."));
+    }
+  }
+
+  return result;
+}
+
+/******************************************************************************
+   PRIVATE MEMBER FUNCTIONS
+ ******************************************************************************/
+
+bool NotecardConnectionHandler::armInterrupt (void) const
+{
+  bool result;
+
+  if (J *req = _notecard.newRequest("card.attn")) {
+    JAddStringToObject(req, "mode","rearm,files");
+    if (J *files = JAddArrayToObject(req, "files")) {
+      JAddItemToArray(files, JCreateString(NOTEFILE_SECURE_INBOUND));
+      if (J *rsp = _notecard.requestAndResponse(req)) {
+        // Check the response for errors
+        if (NoteResponseError(rsp)) {
+          const char *err = JGetString(rsp, "err");
+          Debug.print(DBG_ERROR, F("%s\n"), err);
+          result = false;
+        } else {
+          result = true;
+        }
+        JDelete(rsp);
+      } else {
+        Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+        result = false;
+      }
+    } else {
+      Debug.print(DBG_ERROR, "Failed to allocate request: card.attn:files");
+      JFree(req);
+      result = false;
+    }
+  } else {
+    Debug.print(DBG_ERROR, "Failed to allocate request: card.attn");
+    result = false;
+  }
+
+  return result;
+}
+
+bool NotecardConnectionHandler::configureConnection (bool connect_) const
+{
+  bool result;
+
+  if (J *req = _notecard.newRequest("hub.set")) {
+    // Only update the product if it is not empty or the default value
+    if (_project_uid.length() > 0 && _project_uid != "com.domain.you:product") {
+      JAddStringToObject(req, "product", _project_uid.c_str());
+    }
+
+    // Configure the connection mode based on the `connect_` parameter
+    if (connect_) {
+      JAddStringToObject(req, "mode", "continuous");
+      JAddIntToObject(req, "inbound", _inbound_polling_interval_min);
+      JAddBoolToObject(req, "sync", true);
+    } else {
+      JAddStringToObject(req, "mode", "periodic");
+      JAddIntToObject(req, "inbound", NO_INBOUND_POLLING);
+      JAddIntToObject(req, "outbound", -1);
+      JAddStringToObject(req, "vinbound", "-");
+      JAddStringToObject(req, "voutbound", "-");
+    }
+
+    // Send the request to the Notecard
+    if (J *rsp = _notecard.requestAndResponseWithRetry(req, 30)) {
+      // Check the response for errors
+      if (NoteResponseError(rsp)) {
+        const char *err = JGetString(rsp, "err");
+        Debug.print(DBG_ERROR, F("%s"), err);
+        result = false;
+      } else {
+        result = true;
+      }
+      JDelete(rsp);
+    } else {
+      Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+      result = false; // Assume the worst
+    }
+  } else {
+    Debug.print(DBG_ERROR, "Failed to allocate request: hub.set");
+    result = false; // Assume the worst
+  }
+
+  return result;
+}
+
+uint_fast8_t NotecardConnectionHandler::connected (void) const
+{
+  NotecardConnectionStatus result;
+
+  // Query the connection status from the Notecard
+  if (J *rsp = _notecard.requestAndResponse(_notecard.newRequest("hub.status"))) {
+    // Ensure the transaction doesn't return an error
+    if (NoteResponseError(rsp)) {
+      const char *err = JGetString(rsp, "err");
+      Debug.print(DBG_ERROR, F("%s"),err);
+      result.notecard_error = true;
+    } else {
+      // Parse the transport connection status
+      result.transport_connected = (strstr(JGetString(rsp,"status"),"{connected}") != nullptr);
+
+      // Parse the status of the connection to Notehub
+      result.connected_to_notehub = JGetBool(rsp,"connected");
+
+      // Set the Notecard error status
+      result.notecard_error = false;
+      result.host_error = false;
+    }
+
+    // Free the response
+    JDelete(rsp);
+  } else {
+    Debug.print(DBG_ERROR, F("Failed to acquire Notecard connection status."));
+    result.transport_connected = false;
+    result.connected_to_notehub = false;
+    result.notecard_error = false;
+    result.host_error = true;
+  }
+
+  return result;
+}
+
+J * NotecardConnectionHandler::getNote (bool pop_) const
+{
+  J * result;
+
+  // Look for a Note in the NOTEFILE_SECURE_INBOUND file
+  if (J *req = _notecard.newRequest("note.get")) {
+    JAddStringToObject(req, "file", NOTEFILE_SECURE_INBOUND);
+    if (pop_) {
+      JAddBoolToObject(req, "delete", true);
+    }
+    if (J *note = _notecard.requestAndResponse(req)) {
+      // Ensure the transaction doesn't return an error
+      if (NoteResponseError(note)) {
+        const char *jErr = JGetString(note, "err");
+        if (NoteErrorContains(jErr, "{note-noexist}")) {
+          // The Notefile is empty, thus no Note is available.
+          if (_en_hw_int) {
+            armInterrupt();
+          }
+        } else {
+          // Any other error indicates that we were unable to
+          // retrieve a Note, therefore no Note is available.
+        }
+        result = nullptr;
+        JDelete(note);
+      } else {
+        // The Note was successfully retrieved, and it now
+        // becomes the callers responsibility to free it.
+        result = note;
+      }
+    } else {
+      Debug.print(DBG_ERROR, F("Failed to receive response from Notecard."));
+      result = nullptr;
+    }
+  } else {
+    Debug.print(DBG_ERROR, "Failed to allocate request: note.get");
+    // Failed to retrieve a Note, therefore no Note is available.
+    result = nullptr;
+  }
+
+  return result;
+}
+
+bool NotecardConnectionHandler::updateUidCache (void)
+{
+  bool result;
+
+  // This operation is safe to perform before a sync has occurred, because the
+  // Notecard UID is static and the cloud value of Serial Number is strictly
+  // informational with regard to the host firmware operations.
+
+  // Read the Notecard UID from the Notehub configuration
+  if (J *rsp = _notecard.requestAndResponse(_notecard.newRequest("hub.get"))) {
+    // Check the response for errors
+    if (NoteResponseError(rsp)) {
+      const char *err = JGetString(rsp, "err");
+      Debug.print(DBG_ERROR, F("Failed to read Notecard UID"));
+      Debug.print(DBG_ERROR, F("Error: %s"), err);
+      result = false;
+    } else {
+      _notecard_uid = JGetString(rsp, "device");
+      _device_id = JGetString(rsp, "sn");
+      _device_id = (_device_id.length() ? _device_id : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
+      Debug.print(DBG_DEBUG, F("NotecardConnectionHandler::%s updated cache with Notecard UID: <%s> and Arduino Device ID: <%s>"), __FUNCTION__, _notecard_uid.c_str(), _device_id.c_str());
+      result = true;
+    }
+    JDelete(rsp);
+  } else {
+    Debug.print(DBG_ERROR, F("Failed to read Notecard UID"));
+    result = false;
+  }
+
+  return result;
+}
+
+#endif /* BOARD_HAS_NOTECARD */
diff --git a/src/NotecardConnectionHandler.h b/src/NotecardConnectionHandler.h
new file mode 100644
index 00000000..c758030f
--- /dev/null
+++ b/src/NotecardConnectionHandler.h
@@ -0,0 +1,341 @@
+/*
+  This file is part of the ArduinoIoTCloud library.
+
+  Copyright 2024 Blues (http://www.blues.com/)
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+*/
+
+#ifndef ARDUINO_NOTECARD_CONNECTION_HANDLER_H_
+#define ARDUINO_NOTECARD_CONNECTION_HANDLER_H_
+
+/******************************************************************************
+   INCLUDE
+ ******************************************************************************/
+
+#include <stdint.h>
+
+#include <Arduino.h>
+#include <Notecard.h>
+#include <Wire.h>
+
+#include "ConnectionHandlerInterface.h"
+
+/******************************************************************************
+   DEFINES
+ ******************************************************************************/
+
+#define NOTECARD_CONNECTION_HANDLER_VERSION_MAJOR 1
+#define NOTECARD_CONNECTION_HANDLER_VERSION_MINOR 0
+#define NOTECARD_CONNECTION_HANDLER_VERSION_PATCH 0
+
+#define NOTECARD_CONNECTION_HANDLER_VERSION NOTE_C_STRINGIZE(NOTECARD_CONNECTION_HANDLER_VERSION_MAJOR) "." NOTE_C_STRINGIZE(NOTECARD_CONNECTION_HANDLER_VERSION_MINOR) "." NOTE_C_STRINGIZE(NOTECARD_CONNECTION_HANDLER_VERSION_PATCH)
+
+/******************************************************************************
+   CLASS DECLARATION
+ ******************************************************************************/
+
+/**
+ * @brief The NotecardConnectionHandler class
+ *
+ * The NotecardConnectionHandler class is a concrete implementation of the
+ * ConnectionHandler interface that provides connectivity to the Arduino IoT
+ * Cloud using a Notecard.
+ */
+class NotecardConnectionHandler final : public ConnectionHandler
+{
+  public:
+    /**
+     * @brief The manner in which the Notecard is synchronized with Notehub
+     *
+     * The SyncType enum defines the valid types of synchronization operations
+     * that can be performed by the NotecardConnectionHandler class.
+     *
+     * @par
+     * - Full - synchronize both the inbound and outbound queues.
+     * - Inbound - synchronize only the inbound queues.
+     * - Outbound - synchronize only the outbound queues.
+     */
+    enum class SyncType : uint8_t {
+      Full,
+      Inbound,
+      Outbound,
+    };
+
+    /**
+     * @brief The type of topic to be used for R/W operations
+     *
+     * The Notecard uses topics to identify the target of a read or write
+     * operation. The TopicType enum defines the valid types of topics.
+     *
+     * @par
+     * - Command - used to interact with the Arduino IoT Cloud.
+     * - Thing - used to send application data to the Arduino IoT Cloud.
+     */
+    enum class TopicType : uint8_t {
+      Invalid = 0,
+      Command,
+      Thing,
+    };
+
+    /**
+     * @brief The error codes for communicating with the Notecard
+     *
+     * The NotecardCommunicationError enum defines the error codes that can be
+     * returned by the NotecardConnectionHandler class.
+     *
+     * @par
+     * - NOTECARD_ERROR_NONE - No error occurred.
+     * - NOTECARD_ERROR_NO_DATA_AVAILABLE - No data is available.
+     * - NOTECARD_ERROR_GENERIC - A generic error occurred.
+     * - HOST_ERROR_OUT_OF_MEMORY - The host is out of memory.
+     */
+    typedef enum {
+      NOTECARD_ERROR_NONE                 = 0,
+      NOTECARD_ERROR_NO_DATA_AVAILABLE    = -1,
+      NOTECARD_ERROR_GENERIC              = -2,
+      HOST_ERROR_OUT_OF_MEMORY            = -3,
+    } NotecardCommunicationError;
+
+    /**
+     * @brief The default timeout for the Notecard to connect to Notehub
+     */
+    static const uint32_t NOTEHUB_CONN_TIMEOUT_MS = 185000;
+
+    /**
+     * @brief The I2C constructor for the Notecard
+     *
+     * @param project_uid[in] The project UID of the related Notehub account
+     * @param i2c_address[in] The I2C address of the Notecard
+     * @param i2c_max[in]     The maximum I2C transaction size (MTU)
+     * @param wire[in]        The I2C bus to use
+     * @param keep_alive[in]  Keep the connection alive if connection to Notehub drops
+     */
+    NotecardConnectionHandler(
+      const String & project_uid,
+      uint32_t i2c_address = NOTE_I2C_ADDR_DEFAULT,
+      uint32_t i2c_max = NOTE_I2C_MAX_DEFAULT,
+      TwoWire & wire = Wire,
+      bool keep_alive = true
+    );
+
+    /**
+     * @brief The UART constructor for the Notecard
+     *
+     * @param project_uid[in] The project UID of the related Notehub account
+     * @param serial[in]      The serial port to use
+     * @param baud[in]        The baud rate of the serial port
+     * @param keep_alive[in]  Keep the connection alive if connection to Notehub drops
+     */
+    NotecardConnectionHandler(
+      const String & project_uid,
+      HardwareSerial & serial,
+      uint32_t baud = 9600,
+      bool keep_alive = true
+    );
+
+    /**
+     * @brief Disable hardware interrupts
+     *
+     * When hardware interrupts are disabled, the `NotecardConnectionHandler`
+     * must be polled for incoming data. This is necessary when the host
+     * microcontroller is unable to use the ATTN pin of the Notecard.
+     */
+    inline void disableHardwareInterrupts (void) {
+      _en_hw_int = false;
+    }
+
+    /**
+     * @brief Enable hardware interrupts
+     *
+     * Hardware interrupts allow the `NotecardConnectionHandler` to leverage the
+     * ATTN pin of the Notecard. This improves the responsiveness of the
+     * `NotecardConnectionHandler` by eliminating the need for the host
+     * microcontroller to poll the Notecard for incoming data.
+     */
+    inline void enableHardwareInterrupts (void) {
+      _en_hw_int = true;
+    }
+
+    /**
+     * @brief Get the Arduino IoT Cloud Device ID
+     *
+     * The Arduino IoT Cloud Device ID is set as the serial number of the
+     * Notecard when the device is provisioned in Notehub. The serial number is
+     * updated on each sync between the Notecard and Notehub and cached by the
+     * Notecard. As a result, this value can lag behind the actual value of the
+     * Arduino IoT Cloud Device ID used by the Notehub. However, this value is
+     * typically unchanged during the life of the Notecard, so this is rarely,
+     * if ever, an issue.
+     *
+     * @return The Arduino IoT Cloud Device ID
+     */
+    inline const String & getDeviceId (void) {
+      check(); // Ensure the connection to the Notecard is initialized
+      return _device_id;
+    }
+
+    /**
+     * @brief Get the Notecard object
+     *
+     * The Notecard object is used to interact with the Notecard. This object
+     * provides methods to read and write data to the Notecard, as well as
+     * methods to configure the Notecard.
+     *
+     * @return The Notecard object
+     */
+    inline const Notecard & getNotecard (void) {
+      return _notecard;
+    }
+
+    /**
+     * @brief Get the Notecard Device ID
+     *
+     * The Notecard Device ID is the unique identifier of the Notecard. This
+     * value is set at time of manufacture, and is used to identify the Notecard
+     * in Notehub.
+     *
+     * @return The Notecard Device ID
+     */
+    inline const String & getNotecardUid (void) {
+      check(); // Ensure the connection to the Notecard is initialized
+      return _notecard_uid;
+    }
+
+    /**
+     * @brief Get the topic type of the most recent R/W operations
+     *
+     * @return The current topic type
+     *
+     * @see TopicType
+     */
+    TopicType getTopicType (void) const {
+      return _topic_type;
+    }
+
+    /**
+     * @brief Initiate a synchronization operation with Notehub
+     *
+     * The Notecard maintains two queues: an inbound queue and an outbound
+     * queue. The inbound queue is used to receive data from Notehub, while the
+     * outbound queue is used to send data to Notehub. This method initiates a
+     * synchronization operation between the Notecard and Notehub.
+     *
+     * As the name implies, this method is asynchronous and will only initiate
+     * the synchronization operation. The actual synchronization operation will
+     * be performed by the Notecard in the background.
+     *
+     * @param type[in] The type of synchronization operation to perform
+     * @par
+     * - SyncType::Full - synchronize both the inbound and outbound queues (default)
+     * - SyncType::Inbound - synchronize only the inbound queues.
+     * - SyncType::Outbound - synchronize only the outbound queues.
+     *
+     * @return 0 if successful, otherwise an error code
+     *
+     * @see SyncType
+     * @see NotecardCommunicationError
+     */
+    int initiateNotehubSync (SyncType type = SyncType::Full) const;
+
+    /**
+     * @brief Set the inbound polling interval (in minutes)
+     *
+     * A cellular Notecard will receive inbound traffic from the Arduino IoT
+     * Cloud in real-time. As such, the polling interval is used as a fail-safe
+     * to ensure the Notecard is guaranteed to receive inbound traffic at the
+     * interval specified by this method.
+     *
+     * Alternatively, a LoRa (or Satellite) Notecard does not maintain a
+     * continuous connection, and therefore must rely on the polling interval to
+     * establish the maximum acceptable delay before receiving any unsolicited,
+     * inbound traffic from the Arduino IoT Cloud. The polling interval must
+     * balance the needs of the application against the regulatory limitations
+     * of LoRa (or bandwidth limitations and cost of Satellite).
+     *
+     * LoRaWAN Fair Use Policy:
+     * https://www.thethingsnetwork.org/forum/t/fair-use-policy-explained/1300
+     *
+     * @param interval_min[in] The inbound polling interval (in minutes)
+     *
+     * @note Set the interval to 0 to disable inbound polling.
+     */
+    inline void setNotehubPollingInterval (int32_t interval_min) {
+      _inbound_polling_interval_min = (interval_min ? interval_min : -1);
+    }
+
+    /**
+     * @brief Set the topic type for R/W operations
+     *
+     * @param topic[in] The topic type
+     * @par
+     * - TopicType::Command - used to interact with the Arduino IoT Cloud.
+     * - TopicType::Thing - used to send application data to the Arduino IoT Cloud.
+     *
+     * @see TopicType
+     */
+    void setTopicType (TopicType topic) {
+      _topic_type = topic;
+    }
+
+    /**
+     * @brief Set the WiFi credentials to be used by the Notecard
+     *
+     * @param ssid[in] The SSID of the WiFi network
+     * @param pass[in] The password of the WiFi network
+     *
+     * @return 0 if successful, otherwise an error code
+     *
+     * @note This method is only applicable when using a Wi-Fi capable Notecard,
+     *       and is unnecessary when using a Notecard with cellular connectivity.
+     *       If the Notecard is not Wi-Fi capable, this method will be a no-op.
+     *
+     * @see NotecardCommunicationError
+     */
+    int setWiFiCredentials (const String & ssid, const String & pass);
+
+    // ConnectionHandler interface
+    virtual bool available() override;
+    virtual unsigned long getTime() override;
+    virtual int read() override;
+    virtual int write(const uint8_t *buf, size_t size) override;
+
+  protected:
+
+    virtual NetworkConnectionState update_handleInit         () override;
+    virtual NetworkConnectionState update_handleConnecting   () override;
+    virtual NetworkConnectionState update_handleConnected    () override;
+    virtual NetworkConnectionState update_handleDisconnecting() override;
+    virtual NetworkConnectionState update_handleDisconnected () override;
+
+  private:
+
+    // Private members
+    Notecard _notecard;
+    String _device_id;
+    String _notecard_uid;
+    String _project_uid;
+    HardwareSerial * _serial;
+    TwoWire * _wire;
+    uint8_t * _inbound_buffer;
+    uint32_t _conn_start_ms;
+    uint32_t _i2c_address;
+    uint32_t _i2c_max;
+    uint32_t _inbound_buffer_index;
+    uint32_t _inbound_buffer_size;
+    int32_t _inbound_polling_interval_min;
+    uint32_t _uart_baud;
+    bool _en_hw_int;
+    TopicType _topic_type;
+
+    // Private methods
+    bool armInterrupt (void) const;
+    bool configureConnection (bool connect) const;
+    uint_fast8_t connected (void) const;
+    J * getNote (bool pop = false) const;
+    bool updateUidCache (void);
+};
+
+#endif /* ARDUINO_NOTECARD_CONNECTION_HANDLER_H_ */