From 4f50b120d6f8454c01e9b9f5127a69ed4b80d991 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" 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 + #include "arduino_secrets.h" -#include +#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" 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 /* ... */ -#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 // MUST include this first to enable Notecard support +#include + +#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 |------| Notecard | ( ( Wireless ) ) | Notehub |------| 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 -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 #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 () + #define BOARD_HAS_NOTECARD + #endif +#endif + #include +#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 +#include +#include + +/****************************************************************************** + 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(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(_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(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 + +#include +#include +#include + +#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_ */ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy