From 61ff68f7ad7587bdc268f57a32d60a3de3e6f482 Mon Sep 17 00:00:00 2001 From: brentru Date: Thu, 22 Oct 2020 10:52:17 -0400 Subject: [PATCH] add src/ --- src/Wippersnapper.cpp | 755 ++++++++++++++++++ src/Wippersnapper.h | 193 +++++ src/Wippersnapper_Boards.h | 36 + src/Wippersnapper_Networking.h | 28 + .../Wippersnapper_AIRLIFT.h | 190 +++++ 5 files changed, 1202 insertions(+) create mode 100644 src/Wippersnapper.cpp create mode 100644 src/Wippersnapper.h create mode 100644 src/Wippersnapper_Boards.h create mode 100644 src/Wippersnapper_Networking.h create mode 100644 src/network_interfaces/Wippersnapper_AIRLIFT.h diff --git a/src/Wippersnapper.cpp b/src/Wippersnapper.cpp new file mode 100644 index 00000000..677972e0 --- /dev/null +++ b/src/Wippersnapper.cpp @@ -0,0 +1,755 @@ +/*! + * @file Wippersnapper.cpp + * + * @mainpage Adafruit Wippersnapper Wrapper + * + * @section intro_sec Introduction + * + * This is the documentation for Adafruit's Wippersnapper wrapper for the + * Arduino platform. It is designed specifically to work with the + * Adafruit IO+ Wippersnapper IoT platform. + * + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * @section dependencies Dependencies + * + * This library depends on + * Adafruit_Sensor being present on your system. Please make sure you have + * installed the latest version before using this library. + * + * @section author Author + * + * Written by Brent Rubell for Adafruit Industries. + * + * @section license License + * + * BSD license, all text here must be included in any redistribution. + * + */ + +#include "Wippersnapper.h" + +ws_board_status_t _boardStatus; // TODO: move to header + +uint16_t Wippersnapper::bufSize; +uint8_t Wippersnapper::_buffer[128]; +char Wippersnapper:: _value[45]; +Timer<16U, &millis, char *> Wippersnapper::t_timer; +Wippersnapper::pinInfo Wippersnapper::ws_pinInfo; +char Wippersnapper::timerPin[3]; +/**************************************************************************/ +/*! + @brief Instantiates the Wippersnapper client object. + @param aio_username + Adafruit IO Username. + @param aio_key + Adafruit IO Active Key. +*/ +/**************************************************************************/ +Wippersnapper::Wippersnapper(const char *aio_username, const char *aio_key) { + _mqtt = 0; // MQTT Client object + + _username = aio_username; + _key = aio_key; + + // TODO: Remove! + _deviceId = "myDevice"; // Adafruit IO+ device name + _hw_vid = 0; // Hardware's usb vendor id + _hw_pid = 0; // Hardware's usb product id + + // Reserved MQTT Topics // + _topic_description = 0; + _topic_description_status = 0; + _topic_signals_in = 0; + _topic_signals_out = 0; + + //_init(); +} + +/**************************************************************************/ +/*! + @brief Wippersnapper destructor +*/ +/**************************************************************************/ +Wippersnapper::~Wippersnapper() { + // re-allocate topics + free(_topic_description); + free(_topic_signals_in); + free(_topic_signals_out); +} + +/**************************************************************************/ +/*! + @brief This function is the core of the createSignalRequest encoding process. + It handles the top-level pb_field_t array manually, in order to encode + a correct field tag before the message. The pointer to MsgType_fields + array is used as an unique identifier for the message type. +*/ +/**************************************************************************/ +bool Wippersnapper::encode_unionmessage(pb_ostream_t *stream, const pb_msgdesc_t *messagetype, void *message) +{ + pb_field_iter_t iter; + + if (!pb_field_iter_begin(&iter, signal_v1_CreateSignalRequest_fields, message)) + return false; + + do + { + if (iter.submsg_desc == messagetype) + { + if (!pb_encode_tag_for_field(stream, &iter)) + return false; + + return pb_encode_submessage(stream, messagetype, message); + } + } while (pb_field_iter_next(&iter)); + + return false; +} + +/****************************************************************************/ +/*! + @brief ISR which reads the value of a digital pin and updates pinInfo. +*/ +/****************************************************************************/ +bool Wippersnapper::cbDigitalRead(char *pinName) { + BC_DEBUG_PRINT("cbDigitalRead(");BC_DEBUG_PRINT(pinName);BC_DEBUG_PRINTLN(")"); + + // Read and store pinName into struct. + ws_pinInfo.pinValue = digitalRead((unsigned)atoi(pinName)); + + // debug TODO remove + BC_DEBUG_PRINT("Pin Values: "); BC_DEBUG_PRINT(ws_pinInfo.pinValue); + BC_DEBUG_PRINT(" "); BC_DEBUG_PRINTLN(ws_pinInfo.prvPinValue); + return true; // repeat every xMS +} + +/**************************************************************************/ +/*! + @brief Returns if succcessfully sent pin event to MQTT broker, + otherwise false; +*/ +/**************************************************************************/ +bool Wippersnapper::sendPinEvent() { + uint8_t buffer[128]; // message buffer, TODO: Make this a shared buffer + bool status = false; + + // create output stream for buffer + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + //pb_ostream_t stream = {0}; + + // Create PinEventRequest message type + pin_v1_PinEventRequest msg; + + // Encode payload + strcpy(msg.pin_name, "D"); // TODO: hotfix for broker to identify in desc., remove + strcat(msg.pin_name, timerPin); + itoa(ws_pinInfo.pinValue, msg.pin_value, 10); + + // Encode PinEventRequest message + status = encode_unionmessage(&stream, pin_v1_PinEventRequest_fields, &msg); + + if (!status) + { + Serial.println("Encoding failed!"); + return false; + } + + Serial.print("Encoded size is: ");Serial.println(stream.bytes_written); + + _mqtt->publish(_topic_signals_in, buffer, stream.bytes_written, 0); + return true; +} + +/**************************************************************************/ +/*! + @brief Executes pin events from the broker +*/ +/**************************************************************************/ +// Process pin events from the broker +bool Wippersnapper::pinEvent() { + // strip "D" or "A" from "circuitpython-style" pin_name + char* pinName = signalMessage.payload.pin_event.pin_name + 1; + // Set pin value + BC_DEBUG_PRINT("Setting ")BC_DEBUG_PRINT(atoi(pinName)); + BC_DEBUG_PRINT(" to ");BC_DEBUG_PRINTLN(atoi(signalMessage.payload.pin_event.pin_value)); + digitalWrite(atoi(pinName), atoi(signalMessage.payload.pin_event.pin_value)); + return true; +} + +/**************************************************************************/ +/*! + @brief Configures a pin's mode, direction, pull and period. +*/ +/**************************************************************************/ +bool Wippersnapper::pinConfig() +{ + BC_DEBUG_PRINT("Pin Name: ");BC_DEBUG_PRINTLN(signalMessage.payload.pin_config.pin_name); + BC_DEBUG_PRINT("Mode: ");BC_DEBUG_PRINTLN(signalMessage.payload.pin_config.mode); + BC_DEBUG_PRINT("Direction : ");BC_DEBUG_PRINTLN(signalMessage.payload.pin_config.direction); + BC_DEBUG_PRINT("Pull enabled: ");BC_DEBUG_PRINTLN(signalMessage.payload.pin_config.pull); + + ws_pinInfo.PinNameFull = signalMessage.payload.pin_config.pin_name; + // strip "D" or "A" from "circuitpython-style" pin_name + char* pinName = signalMessage.payload.pin_config.pin_name + 1; + ws_pinInfo.pinName = pinName; + + // TODO: Check for pullup, configure! + + // Configure pin mode and direction + switch(signalMessage.payload.pin_config.mode) { + case pin_v1_ConfigurePinRequest_Mode_MODE_ANALOG: + if (signalMessage.payload.pin_config.direction == pin_v1_ConfigurePinRequest_Direction_DIRECTION_INPUT) { + BC_DEBUG_PRINTLN("* Configuring Analog input pin."); + } else { + BC_DEBUG_PRINTLN("* Configuring Analog output pin."); + } + break; + case pin_v1_ConfigurePinRequest_Mode_MODE_DIGITAL: + // TODO: _INPUT is incorrect, should be 0x0 not 0x1 + if (signalMessage.payload.pin_config.direction == 0) { + BC_DEBUG_PRINTLN("* Configuring digital input pin"); + long timerMs = signalMessage.payload.pin_config.period; + BC_DEBUG_PRINTLN(timerMs); + strcpy(timerPin, pinName); + auto task = t_timer.every(timerMs, cbDigitalRead, timerPin); + } + BC_DEBUG_PRINTLN("Configuring digital pin direction"); + pinMode(atoi(ws_pinInfo.pinName), signalMessage.payload.pin_config.direction); + break; + default: + BC_DEBUG_PRINTLN("Unable to obtain pin configuration from message.") + return false; + break; + } + // TODO: Replace this with a return from within the switch case's calling methods + return true; +} + +/**************************************************************************/ +/*! + @brief Executes when signal topic receives a new message and copies + payload and payload length. +*/ +/**************************************************************************/ +void Wippersnapper::cbSignalTopic(char *data, uint16_t len) { + BC_DEBUG_PRINTLN("cbSignalTopic()"); + memcpy(_buffer, data, len); + bufSize = len; +} + +/**************************************************************************/ +/*! + @brief Decodes a signal buffer protobuf message. + NOTE: Should be executed in-order after a new _buffer is recieved. +*/ +/**************************************************************************/ +bool Wippersnapper::decodeSignalMessage() { + // create a stream which reads from buffer + pb_istream_t stream = pb_istream_from_buffer(_buffer, bufSize); + // decode the message + bool status; + status = pb_decode(&stream, signal_v1_CreateSignalRequest_fields, &signalMessage); + + if (!status) { + BC_DEBUG_PRINTLN("Unable to decode signal message"); + return false; + } + return true; +} + +/**************************************************************************/ +/*! + @brief Calls a function handler provided a signal message's payload + type. +*/ +/**************************************************************************/ +bool Wippersnapper::executeSignalMessageEvent() { + // Executes signal message event based on payload type + switch(signalMessage.which_payload) { + case signal_v1_CreateSignalRequest_pin_config_tag: + Serial.println("DEBUG: Pin config callback"); + pinConfig(); + break; + case signal_v1_CreateSignalRequest_pin_event_tag: + Serial.println("DEBUG: Pin event callback"); + pinEvent(); + break; + case signal_v1_CreateSignalRequest_sensor_config_tag: + Serial.println("DEBUG: Sensor config callback"); + break; + case signal_v1_CreateSignalRequest_sensor_event_tag: + Serial.println("DEBUG: Sensor event callback"); + break; + case signal_v1_CreateSignalRequest_location_request_tag: + Serial.println("DEBUG: Location request callback"); + break; + default: + return false; + break; + } + return true; +} + +/**************************************************************************/ +/*! + @brief Executes when the broker publishes a response to the + client's board description message. +*/ +/**************************************************************************/ +void cbDescriptionStatus(char *data, uint16_t len) { + BC_DEBUG_PRINTLN("cbDescriptionStatus()"); + uint8_t buffer[len]; + memcpy(buffer, data, len); + + // init. CreateDescriptionResponse message + description_v1_CreateDescriptionResponse message = description_v1_CreateDescriptionResponse_init_zero; + // create input stream for buffer + pb_istream_t stream = pb_istream_from_buffer(buffer, len); + // decode the stream + bool status; + status = pb_decode(&stream, description_v1_CreateDescriptionResponse_fields, &message); + + if (!status) { + BC_DEBUG_PRINTLN("Error decoding description status message!"); + } + + // set board status + switch (message.response) { + case description_v1_CreateDescriptionResponse_Response_RESPONSE_OK: + _boardStatus = BC_BOARD_DEF_OK; + break; + case description_v1_CreateDescriptionResponse_Response_RESPONSE_VID_NOT_FOUND: + _boardStatus = BC_BOARD_DEF_INAVLID_VID; + break; + case description_v1_CreateDescriptionResponse_Response_RESPONSE_PID_NOT_FOUND: + _boardStatus = BC_BOARD_DEF_INVALID_PID; + break; + case description_v1_CreateDescriptionResponse_Response_RESPONSE_UNSPECIFIED: + _boardStatus = BC_BOARD_DEF_UNSPECIFIED; + break; + default: + _boardStatus = BC_BOARD_DEF_UNSPECIFIED; + } + + BC_DEBUG_PRINT("Board def. response: ") + BC_DEBUG_PRINTLN(_boardStatus); +} + +/**************************************************************************/ +/*! + @brief Generates Wippersnapper feeds. +*/ +/**************************************************************************/ +void Wippersnapper::generate_feeds() { + + // Check and set network iface UID + BC_DEBUG_PRINT(_boardId); + setUID(); + for (int i = 0; i < sizeof(_uid); i++) { + BC_DEBUG_PRINTLN(_uid[i]); + } + + // TODO: Simplify this + // Strip the bottom 3 bytes from the UID + uint8_t uid[3]; + + BC_DEBUG_PRINTLN("Bottom 3 UID"); + for (int i = 5; i > 2; i--) { + uid[6-1-i] = _uid[i]; + } + // free space + delete [] _uid; + BC_DEBUG_PRINTLN("uid2"); + for (int i = 0; i < 3; i++) { + BC_DEBUG_PRINTLN(uid[i]); + } + + // Assign board type, defined at compile-time + _boardId = BOARD_ID; + + // Assign board type info + // TODO: This should move somewhere else... + _hw_vid = USB_VID; + _hw_pid = USB_PID; + + // dynamically allocate memory for reserved topics + _topic_description = (char *)malloc(sizeof(char) * strlen(_username) + strlen(TOPIC_DESCRIPTION) + 1); + + // TODO: These should be built using the UID + _topic_description_status = (char *)malloc(sizeof(char) * strlen(_username) + strlen(TOPIC_SIGNALS) + strlen("/devices/") + strlen("/status") + 1); + _topic_signals_in = (char *)malloc(sizeof(char) * strlen(_deviceId) + strlen(TOPIC_SIGNALS) + strlen("/devices/in") + 1); + _topic_signals_out = (char *)malloc(sizeof(char) * strlen(_deviceId) + strlen(TOPIC_SIGNALS) + strlen("/devices/out") + 1); + + + // build description topic + if (_topic_description) { + strcpy(_topic_description, _username); + strcat(_topic_description, TOPIC_DESCRIPTION); + } + // build description status topic + if (_topic_description_status) { + strcpy(_topic_description_status, "devices/"); + strcat(_topic_description_status, _deviceId); + strcat(_topic_description_status, TOPIC_DESCRIPTION); + strcat(_topic_description_status, "/status"); + } + // build signals incoming topic + if (_topic_signals_in) { + strcpy(_topic_signals_in, "devices/"); + strcat(_topic_signals_in, _deviceId); + strcat(_topic_signals_in, TOPIC_SIGNALS); + strcat(_topic_signals_in, "in"); + } + + // build signals outgoing topic + if (_topic_signals_out) { + strcpy(_topic_signals_out, "devices/"); + strcat(_topic_signals_out, _deviceId); + strcat(_topic_signals_out, TOPIC_SIGNALS); + strcat(_topic_signals_out, "out"); + } + + +} + +/**************************************************************************/ +/*! + @brief Connects to Adafruit IO+ Wippersnapper broker. +*/ +/**************************************************************************/ +void Wippersnapper::connect() { + BC_DEBUG_PRINTLN("::connect()"); + _status = BC_IDLE; + _boardStatus = BC_BOARD_DEF_IDLE; + + BC_DEBUG_PRINTLN("Generating WS Feeds..."); + generate_feeds(); + // TODO: Print out feeds it generated + + // Subscription to listen to commands from the server + _topic_signals_out_sub = new Adafruit_MQTT_Subscribe(_mqtt, _topic_signals_out); + _topic_signals_out_sub->setCallback(cbSignalTopic); + //_mqtt->subscribe(_topic_signals_out_sub); + + // Publish to outgoing commands channel, server listens to this sub-topic + _topic_signals_in_pub = new Adafruit_MQTT_Publish(_mqtt, _topic_signals_in); + + // Create a subscription to the description status response topic + _topic_description_sub = new Adafruit_MQTT_Subscribe(_mqtt, _topic_description_status); + + // set callback + _topic_description_sub->setCallback(cbDescriptionStatus); + + // subscribe + //_mqtt->subscribe(_topic_description_sub); + + BC_DEBUG_PRINT("Connecting to Wippersnapper."); + + // Connect network interface + _connect(); + + // Wait for connection to broker + while (status() < BC_CONNECTED) { + BC_DEBUG_PRINT("."); + delay(500); + } + BC_DEBUG_PRINTLN("Connected!"); + + // Send hardware description to broker + if (!sendGetHardwareDescription()){ + // TODO: get error types back from function instead of bool, verify against resp. + BC_DEBUG_PRINTLN("Hardware description process failed!"); + } + +} + +/**************************************************************************/ +/*! + @brief Disconnects from Adafruit IO+ Wippersnapper. +*/ +/**************************************************************************/ +void Wippersnapper::disconnect() { + _disconnect(); +} + +/**************************************************************************/ +/*! + @brief Checks and handles network interface connection. +*/ +/**************************************************************************/ +ws_status_t Wippersnapper::checkNetworkConnection(uint32_t timeStart) { + if (status() < BC_NET_CONNECTED) { + BC_DEBUG_PRINTLN("connection failed, reconnecting..."); + unsigned long startRetry = millis(); + while (status() < BC_CONNECTED) { // return an error on timeout + if (millis() - startRetry > 5000) { + return status(); + } + delay(500); + } + if (!sendGetHardwareDescription()){ + // TODO: get error types back from function instead of bool, verify against resp. + return status(); + } + } + return status(); +} + +/**************************************************************************/ +/*! + @brief Checks and handles connection to MQTT broker. +*/ +/**************************************************************************/ +ws_status_t Wippersnapper::checkMQTTConnection(uint32_t timeStart) { + while(mqttStatus() != BC_CONNECTED && millis() - timeStart < 60000) { + } + if (mqttStatus() != BC_CONNECTED) { + return status(); + } + return status(); +} + +/**************************************************************************/ +/*! + @brief Pings MQTT broker to keep connection alive. +*/ +/**************************************************************************/ +void Wippersnapper::ping() { + if (millis() > (_prv_ping + 60000)) { + _mqtt->ping(); + _prv_ping = millis(); + } +} + +/**************************************************************************/ +/*! + @brief Processes incoming commands and handles network connection. +*/ +/**************************************************************************/ +ws_status_t Wippersnapper::run() { + uint32_t timeStart = millis(); + // increment software timer + t_timer.tick(); + + // Check network connection + checkNetworkConnection(timeStart); // TODO: handle this better + // Check and handle MQTT connection + checkMQTTConnection(timeStart); // TODO: handle this better + + // Ping broker if keepalive elapsed + ping(); + + // Process all incoming packets from Wippersnapper MQTT Broker + _mqtt->processPackets(500); + + // Handle incoming signal message + int n; // TODO: decl. in .h instead + // TODO: Sizeof may not work, possibly use bufSize instead + n = memcmp(_buffer, _buffer_state, sizeof(_buffer)); + if (! n == 0) { + BC_DEBUG_PRINTLN("New data in message buffer"); + // Decode signal message + if (! decodeSignalMessage()) { + return status(); + } + if (! executeSignalMessageEvent()) { + BC_DEBUG_PRINTLN("Err: Event failed to execute."); + } + // update _buffer_state with contents of new message + // TODO: Sizeof may not work, possibly use bufSize instead + memcpy(_buffer_state, _buffer, sizeof(_buffer)); + } + // Send updated pin value to broker + if ( ws_pinInfo.pinValue != ws_pinInfo.prvPinValue ) { + BC_DEBUG_PRINT("Pin Values: "); BC_DEBUG_PRINT(ws_pinInfo.pinValue); + BC_DEBUG_PRINT(" "); BC_DEBUG_PRINT(ws_pinInfo.prvPinValue); + sendPinEvent(); + ws_pinInfo.prvPinValue = ws_pinInfo.pinValue; + } + + return status(); +} + +/**************************************************************************/ +/*! + @brief Sends board description message to Wippersnapper +*/ +/**************************************************************************/ +bool Wippersnapper::sendBoardDescription() { + BC_DEBUG_PRINT("Publishing board description..."); + uint8_t buffer[128]; // message stored in this buffer + size_t message_length; + bool status; + + // initialize message definition + description_v1_CreateDescriptionRequest message = description_v1_CreateDescriptionRequest_init_zero; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + + // fill message fields + strcpy(message.machine_name, _deviceId); + message.usb_vid = USB_VID; + message.usb_pid = USB_PID; + + // encode message + status = pb_encode(&stream, description_v1_CreateDescriptionRequest_fields, &message); + message_length = stream.bytes_written; + + // verify message + if (!status) { + BC_DEBUG_PRINTLN("encoding description message failed!"); + //printf("Encoding failed: %s\n", PB_GET_ERROR(&stream)); + return false; + } + + // publish message + _mqtt->publish(_topic_description, buffer, message_length, 0); + BC_DEBUG_PRINTLN("Published!"); + _boardStatus = BC_BOARD_DEF_SENT; + return true; +} + +/***************************************************************************/ +/*! + @brief Sends board description message and verifies broker's response +*/ +/***************************************************************************/ +bool Wippersnapper::sendGetHardwareDescription(){ + // Send hardware characteristics to broker + if (!sendBoardDescription()) { + _boardStatus = BC_BOARD_DEF_SEND_FAILED; + BC_DEBUG_PRINTLN("Unable to send board description to broker"); + return false; + } + + // Verify broker responds OK + BC_DEBUG_PRINTLN("Verifying board definition response") + while (getBoardStatus() != BC_BOARD_DEF_OK) { + BC_DEBUG_PRINT("."); + // TODO: needs a retry+timeout loop here!! + _mqtt->processPackets(500); // run a processing loop + } + return true; +} + +/**************************************************************************/ +/*! + @brief Returns the network status. + @return Wippersnapper network status. +*/ +/**************************************************************************/ +ws_status_t Wippersnapper::status() { + ws_status_t net_status = networkStatus(); + + // if we aren't connected, return network status + if (net_status != BC_NET_CONNECTED) { + _status = net_status; + return _status; + } + + // check mqtt status and return + _status = mqttStatus(); + return _status; +} + +/**************************************************************************/ +/*! + @brief Returns the board definition status + @return Wippersnapper board definition status +*/ +/**************************************************************************/ +ws_board_status_t Wippersnapper::getBoardStatus() { + return _boardStatus; +} + +/**************************************************************************/ +/*! + @brief Checks connection status with Adafruit IO's MQTT broker. + @return True if connected, otherwise False. +*/ +/**************************************************************************/ +ws_status_t Wippersnapper::mqttStatus() { + // if the connection failed, + // return so we don't hammer IO + if (_status == BC_CONNECT_FAILED) { + BC_DEBUG_PRINT("mqttStatus() failed to connect"); + BC_DEBUG_PRINTLN(_mqtt->connectErrorString(_status)); + return _status; + } + + if (_mqtt->connected()) + return BC_CONNECTED; + + // prevent fast reconnect attempts, except for the first time through + if (_last_mqtt_connect == 0 || + millis() - _last_mqtt_connect > 60000) { + _last_mqtt_connect = millis(); + switch (_mqtt->connect(_username, _key)) { + case 0: + return BC_CONNECTED; + case 1: // invalid mqtt protocol + case 2: // client id rejected + case 4: // malformed user/pass + case 5: // unauthorized + return BC_CONNECT_FAILED; + case 3: // mqtt service unavailable + case 6: // throttled + case 7: // banned -> all MQTT bans are temporary, so eventual retry is + // permitted + // delay to prevent fast reconnects and fast returns (backward + // compatibility) + delay(60000); + return BC_DISCONNECTED; + default: + return BC_DISCONNECTED; + } + } + return BC_DISCONNECTED; +} + + +/**************************************************************************/ +/*! + @brief Provide status explanation strings. + @return A pointer to the status string, _status. _status is the BC status + value +*/ +/**************************************************************************/ +const __FlashStringHelper *Wippersnapper::statusText() { + switch (_status) { + // CONNECTING + case BC_IDLE: + return F("Idle. Waiting for connect to be called..."); + case BC_NET_DISCONNECTED: + return F("Network disconnected."); + case BC_DISCONNECTED: + return F("Disconnected from Wippersnapper."); + // FAILURE + case BC_NET_CONNECT_FAILED: + return F("Network connection failed."); + case BC_CONNECT_FAILED: + return F("Wippersnapper connection failed."); + case BC_FINGERPRINT_INVALID: + return F("Wippersnapper SSL fingerprint verification failed."); + case BC_AUTH_FAILED: + return F("Wippersnapper authentication failed."); + // SUCCESS + case BC_NET_CONNECTED: + return F("Network connected."); + case BC_CONNECTED: + return F("Wippersnapper connected."); + case BC_CONNECTED_INSECURE: + return F("Wippersnapper connected. **THIS CONNECTION IS INSECURE** SSL/TLS " + "not supported for this platform."); + case BC_FINGERPRINT_UNSUPPORTED: + return F("Wippersnapper connected over SSL/TLS. Fingerprint verification " + "unsupported."); + case BC_FINGERPRINT_VALID: + return F("Wippersnapper connected over SSL/TLS. Fingerprint valid."); + default: + return F("Unknown status code"); + } +} diff --git a/src/Wippersnapper.h b/src/Wippersnapper.h new file mode 100644 index 00000000..128c6a76 --- /dev/null +++ b/src/Wippersnapper.h @@ -0,0 +1,193 @@ +/*! + * @file BlinkaConnect.h + * + * This is the documentation for Adafruit's BlinkaConnect wrapper for the + * Arduino platform. It is designed specifically to work with the + * Adafruit IO+ BlinkaConnect IoT platform. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Written by Brent Rubell for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ + +#ifndef WIPPERSNAPPER_H +#define WIPPERSNAPPER_H + +// Nanopb +#include +#include +#include +#include +// Protocol buffer messages +#include // description.proto +#include // signal.proto + +// External libraries +#include "Adafruit_MQTT.h" +#include "Arduino.h" +#include + +// Internal libraries +#include "Wippersnapper_Boards.h" + +// Reserved Wippersnapper topics +#define TOPIC_DESCRIPTION "/info/" ///< Device description topic +#define TOPIC_SIGNALS "/signals/" ///< Device signals topic + +#define BC_PRINTER Serial ///< Where debug messages will be printed +#define BC_DEBUG +// Define actual debug output functions when necessary. +#ifdef BC_DEBUG +#define BC_DEBUG_PRINT(...) \ + { BC_PRINTER.print(__VA_ARGS__); } ///< Prints debug output. +#define BC_DEBUG_PRINTLN(...) \ + { BC_PRINTER.println(__VA_ARGS__); } ///< Prints line from debug output. +#else +#define BC_DEBUG_PRINT(...) \ + {} ///< Prints debug output +#define BC_DEBUG_PRINTLN(...) \ + {} ///< Prints line from debug output. +#endif + +// Adafruit IO Status States +typedef enum { + BC_IDLE = 0, // Waiting for connection establishement + BC_NET_DISCONNECTED = 1, // Network disconnected + BC_DISCONNECTED = 2, // Disconnected from Adafruit IO + BC_FINGERPRINT_UNKOWN = 3, // Unknown BC_SSL_FINGERPRINT + + BC_NET_CONNECT_FAILED = 10, // Failed to connect to network + BC_CONNECT_FAILED = 11, // Failed to connect to Adafruit IO + BC_FINGERPRINT_INVALID = 12, // Unknown BC_SSL_FINGERPRINT + BC_AUTH_FAILED = 13, // Invalid Adafruit IO login credentials provided. + BC_SSID_INVALID = + 14, // SSID is "" or otherwise invalid, connection not attempted + + BC_NET_CONNECTED = 20, // Connected to Adafruit IO + BC_CONNECTED = 21, // Connected to network + BC_CONNECTED_INSECURE = 22, // Insecurely (non-SSL) connected to network + BC_FINGERPRINT_UNSUPPORTED = 23, // Unsupported BC_SSL_FINGERPRINT + BC_FINGERPRINT_VALID = 24, // Valid BC_SSL_FINGERPRINT + + BC_BOARD_DESC_INVALID = 25 // Unable to send board description +} ws_status_t; + +// Wippersnapper board definition status +typedef enum { + BC_BOARD_DEF_IDLE, + BC_BOARD_DEF_SEND_FAILED, + BC_BOARD_DEF_SENT, + BC_BOARD_DEF_OK, + BC_BOARD_DEF_INAVLID_VID, + BC_BOARD_DEF_INVALID_PID, + BC_BOARD_DEF_UNSPECIFIED +} ws_board_status_t; + + +class Wippersnapper { + + public: + Wippersnapper(const char *aio_username, const char *aio_key); + virtual ~Wippersnapper(); + + void connect(); + virtual void _connect() = 0; + + void disconnect(); + virtual void _disconnect() = 0; + + void generate_feeds(); // Generate device-specific WS feeds + virtual void setUID() = 0; + + const __FlashStringHelper *statusText(); + virtual ws_status_t networkStatus() = 0; + ws_status_t status(); + ws_status_t mqttStatus(); + ws_board_status_t getBoardStatus(); + + bool sendBoardDescription(); + bool sendGetHardwareDescription(); + + ws_status_t checkNetworkConnection(uint32_t timeStart); + ws_status_t checkMQTTConnection(uint32_t timeStart); + void ping(); + ws_status_t run(); + + bool decodeSignalMessage(); + bool executeSignalMessageEvent(); + bool pinConfig(); + bool pinEvent(); + bool sendPinEvent(); + + static bool cbDigitalRead(char *pinName); + static void cbSignalTopic(char *data, uint16_t len); + + const char *_username; /*!< Adafruit IO Username. */ + const char *_key; /*!< Adafruit IO Key. */ + + static uint16_t bufSize; + static uint8_t _buffer[128]; /*!< Shared buffer to save callback payload */ + uint8_t _buffer_state[128]; /*!< Holds previous contents of static _buffer */ + // Protobuf structs + signal_v1_CreateSignalRequest signalMessage = signal_v1_CreateSignalRequest_init_zero; + // Protobuf helpers + bool encode_unionmessage(pb_ostream_t *stream, const pb_msgdesc_t *messagetype, void *message); + + struct pinInfo { + char *pinName; + char *PinNameFull; + int pinValue; + int prvPinValue; // holds prv. pin state + }; + static pinInfo ws_pinInfo; + + static Timer<16, millis, char *> t_timer; + + static char timerPin[3]; + + private: + void _init(); + + protected: + ws_status_t _status = BC_IDLE; /*!< Adafruit IO connection status */ + uint32_t _last_mqtt_connect = 0; /*!< Previous time when client connected to + Adafruit IO, in milliseconds */ + uint32_t _prv_ping = 0; + + Adafruit_MQTT *_mqtt; /*!< Reference to Adafruit_MQTT, _mqtt. */ + + // PoC Server + //const char *_mqtt_broker = "2.tcp.ngrok.io"; /*!< MQTT Broker URL */ + // uint16_t _mqtt_port = 18653; /*!< MQTT Broker URL */ + + // Staging Server + const char *_mqtt_broker = "io.adafruit.us"; /*!< MQTT Broker URL */ + uint16_t _mqtt_port = 1883; /*!< MQTT Broker URL */ + + const char *_deviceId; /*!< Adafruit IO+ device identifier string */ + const char *_boardId; /*!< Adafruit IO+ board string */ + uint16_t _hw_vid; /*!< USB vendor identifer */ + uint16_t _hw_pid; /*!< USB product identifier */ + uint8_t _uid[6]; + + // MQTT topics + char *_topic_description; /*!< MQTT topic for the device description */ + char *_topic_description_status; /*!< MQTT subtopic carrying the description status resp. from the broker */ + char *_topic_signals_in; /*!< Device -> Server communication channel */ + char *_topic_signals_out; /*!< Server -> Device communication channel */ + + Adafruit_MQTT_Subscribe *_topic_description_sub; + Adafruit_MQTT_Publish *_topic_signals_in_pub; + Adafruit_MQTT_Subscribe *_topic_signals_out_sub; + Adafruit_MQTT_Subscribe *_subscription; + + static char _value[45]; /*!< Data to send back to Wippersnapper, max. IO data len */ + static char _prv_value[45]; /*!< Data to send back to Wippersnapper, max. IO data len */ +}; + +#endif // ADAFRUIT_WIPPERSNAPPER_H \ No newline at end of file diff --git a/src/Wippersnapper_Boards.h b/src/Wippersnapper_Boards.h new file mode 100644 index 00000000..97ffe375 --- /dev/null +++ b/src/Wippersnapper_Boards.h @@ -0,0 +1,36 @@ +/*! + * @file Wippersnapper_Boards.h + * + * This file determines board type at compile-time + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Written by Brent Rubell for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ + +#ifndef ADAFRUIT_WIPPERSNAPPER_BOARDS_H +#define ADAFRUIT_WIPPERSNAPPER_BOARDS_H + +#if defined(ADAFRUIT_PYPORTAL) + #define USB_VID 0x239A + #define USB_PID 0x8036 + #define BOARD_ID "adafruit_pyportal_m4" +#elif defined(ADAFRUIT_METRO_M4_AIRLIFT_LITE) + #define USB_VID 0x239A + #define USB_PID 0x8038 + #define BOARD_ID "adafruit_metro_m4_airliftlite" +#elif defined(ADAFRUIT_METRO_M4_EXPRESS) + #define USB_VID 0x239A + #define USB_PID 0x8021 + #define BOARD_ID "adafruit_metro_m4" +#else + #warning "Board not identified within Wippersnapper_Boards.h!" +#endif + + +#endif // ADAFRUIT_WIPPERSNAPPER_BOARDS_H \ No newline at end of file diff --git a/src/Wippersnapper_Networking.h b/src/Wippersnapper_Networking.h new file mode 100644 index 00000000..49058f89 --- /dev/null +++ b/src/Wippersnapper_Networking.h @@ -0,0 +1,28 @@ +/*! + * @file Wippersnapper_Networking.h + * + * This file includes network interfaces at compile-time. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Written by Brent Rubell for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ + +#ifndef WIPPERSNAPPER_NETWORKING_H +#define WIPPERSNAPPER_NETWORKING_H + +#if defined(ADAFRUIT_METRO_M4_AIRLIFT_LITE) || \ + defined(ADAFRUIT_PYPORTAL) || defined(ADAFRUIT_METRO_M4_EXPRESS) || \ + defined(USE_AIRLIFT) + #include "network_interfaces/Wippersnapper_AIRLIFT.h" + typedef Wippersnapper_AIRLIFT Wippersnapper_WiFi; +#else + #warning "Must define network interface in config.h!" +#endif + +#endif // WIPPERSNAPPER_NETWORKING_H \ No newline at end of file diff --git a/src/network_interfaces/Wippersnapper_AIRLIFT.h b/src/network_interfaces/Wippersnapper_AIRLIFT.h new file mode 100644 index 00000000..47eb4031 --- /dev/null +++ b/src/network_interfaces/Wippersnapper_AIRLIFT.h @@ -0,0 +1,190 @@ +/*! + * @file Wippersnapper_AIRLIFT.h + * + * This is a driver for using the Adafruit AirLift + * ESP32 Co-Processor with Wippersnapper. + * + * The ESP32 uses SPI to communicate. Three lines (CS, ACK, RST) are required + * to communicate with the ESP32. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Written by Brent Rubell for Adafruit Industries. + * + * MIT license, all text here must be included in any redistribution. + * + */ + +#ifndef WIPPERSNAPPER_AIRLIFT_H +#define WIPPERSNAPPER_AIRLIFT_H + +#include "Wippersnapper.h" +#include "Adafruit_MQTT.h" +#include "Adafruit_MQTT_Client.h" +#include "Arduino.h" +#include "SPI.h" +#include "WiFiNINA.h" + +#define NINAFWVER "1.0.0" /*!< min. nina-fw version compatible with this library. */ + +/****************************************************************************/ +/*! + @brief Class for interacting with AirLift Co-Processors. +*/ +/****************************************************************************/ +class Wippersnapper_AIRLIFT : public Wippersnapper { + +public: + /**************************************************************************/ + /*! + @brief Initializes the Adafruit IO class for AirLift devices. + @param deviceId + The Adafruit IO+ device identifier, TODO + @param ssid + The WiFi network's SSID. + @param ssidPassword + The WiFi network's password. + @param ssPin + The ESP32's S.S. pin. + @param ackPin + The ESP32's ACK pin. + @param rstPin + The ESP32's RST pin. + @param gpio0Pin + The ESP32's gpio0 pin. + @param wifi + a SPIClass + */ + /**************************************************************************/ + Wippersnapper_AIRLIFT(const char *aio_user, const char *aio_key, const char *ssid, + const char *ssidPassword, int ssPin, int ackPin, int rstPin, + int gpio0Pin, SPIClass *wifi): Wippersnapper(aio_user, aio_key) { + _wifi = wifi; + _ssPin = ssPin; + _ackPin = ackPin; + _rstPin = rstPin; + _gpio0Pin = gpio0Pin; + _ssid = ssid; + _pass = ssidPassword; + + _mqtt_client = new WiFiClient; + _mqtt = new Adafruit_MQTT_Client(_mqtt_client, _mqtt_broker, _mqtt_port); + } + + /**************************************************************************/ + /*! + @brief Destructor for the Adafruit IO AirLift class. + */ + /**************************************************************************/ + ~Wippersnapper_AIRLIFT() { + if (_mqtt) + delete _mqtt; + } + + /********************************************************/ + /*! + @brief Checks the version of an ESP32 module against + NINAFWVER. Raises an error if the firmware needs to be + upgraded. + */ + /********************************************************/ + void firmwareCheck() { + _fv = WiFi.firmwareVersion(); + if (_fv < NINAFWVER) { + BC_DEBUG_PRINTLN("Please upgrade the firmware on the ESP module to the latest version."); + } + } + + /********************************************************/ + /*! + @brief Gets the AirLift interface's MAC Address. + */ + /********************************************************/ + void setUID() { + WiFi.macAddress(mac); + memcpy(_uid, mac, sizeof(mac)); + } + + /********************************************************/ + /*! + @brief Returns the network status of an ESP32 module. + @return ws_status_t + */ + /********************************************************/ + ws_status_t networkStatus() { + switch (WiFi.status()) { + case WL_CONNECTED: + return BC_NET_CONNECTED; + case WL_CONNECT_FAILED: + return BC_NET_CONNECT_FAILED; + case WL_IDLE_STATUS: + return BC_IDLE; + default: + return BC_NET_DISCONNECTED; + } + } + + /*******************************************************************/ + /*! + @brief Returns the type of network connection used by Wippersnapper + @return AIRLIFT + */ + /*******************************************************************/ + const char *connectionType() { return "AIRLIFT"; } + +protected: + const char *_ssid; + const char *_pass; + String _fv = "0.0.0"; + uint8_t mac[6] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + int _ssPin, _ackPin, _rstPin, _gpio0Pin = -1; + + WiFiClient *_mqtt_client; + + SPIClass *_wifi; + + /**************************************************************************/ + /*! + @brief Establishes a connection with the wireless network. + */ + /**************************************************************************/ + void _connect() { + if (strlen(_ssid) == 0) { + _status = BC_SSID_INVALID; + } else { + // setup ESP32 pins + if (_ssPin != -1) { + WiFi.setPins(_ssPin, _ackPin, _rstPin, _gpio0Pin, _wifi); + } + + // validate up-to-date nina-fw version + firmwareCheck(); + + // disconnect from possible previous connection + _disconnect(); + + // check for esp32 module + if (WiFi.status() == WL_NO_MODULE) { + BC_DEBUG_PRINT("No ESP32 module detected!"); + return; + } + + WiFi.begin(_ssid, _pass); + _status = BC_NET_DISCONNECTED; + } + } + + /**************************************************************************/ + /*! + @brief Disconnects from the wireless network. + */ + /**************************************************************************/ + void _disconnect() { + WiFi.disconnect(); + delay(500); + } +}; + +#endif //WIPPERSNAPPER_AIRLIFT_H \ No newline at end of file