diff --git a/.github/workflows/compile-examples.yml b/.github/workflows/compile-examples.yml index 878d566..c0c5b9a 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 c2759ac..139db7f 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 0000000..e418361 --- /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 0000000..7f90a7c --- /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 0000000..bd2a9d5 --- /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/examples/ConnectionHandlerDemo/ConnectionHandlerDemo.ino b/examples/ConnectionHandlerDemo/ConnectionHandlerDemo.ino index 13ad117..46a0c9d 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 4d9fb7c..f9906f6 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"; diff --git a/keywords.txt b/keywords.txt index ef5227f..68e2be2 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 16239e4..c2ebe2a 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 bb4c0e6..4832726 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 3fdde16..96bcd65 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 94768ea..228827e 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 0000000..5205c4a --- /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 0000000..c758030 --- /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_ */