Particle Projects

May 2020

Particle Particle Webhook to Ubidots


Ubidots is a complete cloud based IoT Platform that can manage customers, devices, and provide data analytics and dashboards (charting).   The Professional subscription is required in order to manage multiple customers and provide them with a private dashboard (50 users, 200 devices, up to 20 channels/variables per device).   The number of users at each subscription tier is the constraint when each customer has only one device (50 users/200 devices; 200 users/1000 devices; ..).  

Ubidots calls a channel a variable, and a channel value a dot.  

 

Ubidots Library

Ubidots.h

Connect Particle Mesh devices to Ubidots

Particle to Ubidots - native webooks vs Ubidots library

 

Multiple Channel / Variable Webhook

Ubidots will accept multiple channels / variables posted simultaneously in a variety of formats.   In all cases, the URL should include the device associated with channels / variables as shown below.  


https://industrial.api.ubidots.com/api/v1.6/devices/{{{PARTICLE_DEVICE_ID}}}/

Below is the most simple JSON string that can be posted to the Ubidots REST API for more than one channel/variable.  


{
"variable-label-1": 2.51,
"variable-label-2": 1.52
}

The Particle code to publish the values for two analog inputs to Ubidots through a Webhook named ubidots.  

Here is an example of sending one value / dot for each of two channels / variables.   Note that if you include metadata such as unit, label, location, etc., then the JSON structure gets more complicated.   The code sample I provide later is for the more simple JSON string.  


{
"variable-label-1":
{
  "value": 1.45,
  "context": {
    "unit": "mV"
  }
},
"variable-label-2":
{
  "value": 1.55,
  "context": {
    "unit": "mV"
  }
}
}

The URL in the form shown below is:


https://industrial.api.ubidots.com/api/v1.6/devices/{{{PARTICLE_DEVICE_ID}}}/

Sample Particle code to generate the output required for the "ubidots" Webhook. See PublishAiVals().


#include "Particle.h"

//////////////////////////////////////////////////////////////////////////////////////////
// Install library: JsonParserGeneratorRK
// Reference: https://github.com/rickkas7/JsonParserGeneratorRK
#include "JsonParserGeneratorRK.h"
///////////////////////////////////////////////////////////////////////////////////
//  Analog Inputs
// Timer for publishing analog input values to the Particle Cloud
const unsigned long TIMER_PUBLISH_AI_INTERVAL_MS = 15000; // Min is 1000 ms
unsigned long timerPublishAiLast = 0;  

// Timer for publishing analog input alarm events to the Particle Cloud
const unsigned long TIMER_AI_ALARM_PUBLISH_INTERVAL_MS = 15000; // Min is 1000 ms
unsigned long timerAiAlarmPublishLast = 0;  

// Bundle all of the analog input data into a structure.
const byte AI_COUNT = 2;
struct analog_inputs_t {
  byte pin;
  String pinName;
  unsigned int ADC;
  unsigned int ADC_offset;
  unsigned long ai_samples;
  double fs_val;
  String fs_unit;
  double mV_to_fs;
  double fs_low_limit;
  double fs_high_limit;
  unsigned long timer_low_ms;
  unsigned long timer_high_ms;
  unsigned long timer_limit_low_ms;
  unsigned long timer_limit_high_ms;
  boolean alarm;
  unsigned long alarm_last_ms;
} arr_ai[AI_COUNT];
///////////////////////////////////////////////////////////////////////////////////
unsigned long publish_error_count = 0;


void setup() {
  Mesh.off(); // Turn the Mesh Radio off

  pinMode(D7, OUTPUT);

  Serial.begin();
  waitFor(Serial.isConnected, 30000);
  Serial.printlnf("System version: %s", (const char*)System.version());
  Serial.printlnf("Free RAM %d", System.freeMemory());

  // Initialize arr_ai
  arr_ai[0].pin = A1;
  arr_ai[0].pinName = "A1";
  arr_ai[1].pin = A2;
  arr_ai[1].pinName = "A2";
  for (int i=0; i<AI_COUNT; i++) {
    pinMode(arr_ai[i].pin, INPUT);
    arr_ai[i].ADC = 0;
    arr_ai[i].fs_val = 0.0;
    arr_ai[i].alarm = false;
    arr_ai[i].alarm_last_ms = millis();
    arr_ai[i].timer_low_ms = millis();  
    arr_ai[i].timer_high_ms = millis();  
    // Move anything below to before for () in order to assign unique values to each analog input.
    arr_ai[i].ADC_offset = 1;               // Calibration correction
    arr_ai[i].mV_to_fs = 0.001;             // Conversion factor to apply to mV analog input reading to full scale
    arr_ai[i].fs_unit = "V";                // Unit for the analog input values in full scale
    arr_ai[i].fs_low_limit = 0.543;         // Full scale values less than fs_low_limit will trigger .alarm
    arr_ai[i].fs_high_limit = 3.300;        // Full scale values greater than fs_high_limit will trigger .alarm
    arr_ai[i].timer_limit_low_ms = 10000;   // Minimum time the value low value .mV_to_fs must be less than .fs_low_limit in order to trigger an alarm
    arr_ai[i].timer_limit_high_ms = 10000;  // Minimum time the high value must persist in order to trigger an alarm
  }

} // setup()


void loop() {

  ReadAnalogInputs();
  PublishAiVals();
  ProcessAiAlarms();

} // loop()


void ReadAnalogInputs() {
  // 12 bit ADC (values between 0 and 4095 or 2^12) or a resolution of 0.8 mV
  // Hardware minimum sample time to read one analog value is 10 microseconds. 
  // Raw ADC values are adjusted by a calibration factor arr_ai_ADC_calib[n].ADCoffset
  // that is determined by bench testing against precision voltage reference. 
  // Update .ADC with the cumulative ADC value (to calculate average over the publish interval).
  // Update .fs_val with the current ADC value and check for alarm conditions.
  for (int i=0; i<AI_COUNT; i++) {
    int ADC = analogRead(arr_ai[i].pin) + arr_ai[i].ADC_offset;
    arr_ai[i].ADC += ADC;
    arr_ai[i].fs_val = double(ADC) * 3300.0 / 4096.0 * arr_ai[i].mV_to_fs;
    arr_ai[i].ai_samples++;
    
    // Reset the low / high value timers & alarm timer if limits are not exceeded and no alarm exists
    if (arr_ai[i].alarm == false) {
      if (arr_ai[i].fs_val > arr_ai[i].fs_low_limit) {
        arr_ai[i].timer_low_ms = millis();
        arr_ai[i].alarm_last_ms = millis();
      }
      if (arr_ai[i].fs_val < arr_ai[i].fs_high_limit) {
        arr_ai[i].timer_high_ms = millis();
        arr_ai[i].alarm_last_ms = millis();
      }
    }

    // Check for an alarm condition (low voltage for longer than .timer_limit_low_ms, or high voltage for longer than .timer_limit_high_ms)
    if (arr_ai[i].fs_val < arr_ai[i].fs_low_limit && millis() - arr_ai[i].timer_low_ms > arr_ai[i].timer_limit_low_ms) {
      arr_ai[i].alarm = true;
    }
    if (arr_ai[i].fs_val > arr_ai[i].fs_high_limit && millis() - arr_ai[i].timer_high_ms > arr_ai[i].timer_limit_high_ms) {
      arr_ai[i].alarm = true;
    }

  } // for 
}  // ReadAnalogInputs()


void PublishAiVals() {
  if (timerPublishAiLast > millis())  timerPublishAiLast = millis();
  if ((millis() - timerPublishAiLast) > TIMER_PUBLISH_AI_INTERVAL_MS) {

    JsonWriterStatic<256> jw;
    {
    JsonWriterAutoObject obj(&jw);
    jw.setFloatPlaces(2);
    for (int i=0; i<AI_COUNT; i++) {
      double fs_val = double(arr_ai[i].ADC) / double(arr_ai[i].ai_samples) * 3300.0 / 4096.0 * arr_ai[i].mV_to_fs;
      jw.insertKeyValue(arr_ai[i].pinName, fs_val);
      //jw.insertKeyValue("field" + String(i), fs_val);
      arr_ai[i].ADC = 0;
      arr_ai[i].ai_samples = 0;
    } // for
    }
    
    Serial.printlnf("jw.getBuffer() = '%s'", jw.getBuffer());
    // output:  jw.getBuffer() = '{"A1":1.61,"A2":2.00}'
    byte PubResult = Particle.publish("ubidots", jw.getBuffer(), PRIVATE);
    if (PubResult == 1) 
      publish_error_count = 0;
    else
      publish_error_count++;

    timerPublishAiLast = millis();
  } // timer
} // PublishAiVals()


void ProcessAiAlarms() {
  // Publish any analog input alarm conditions to the Particle Cloud, choosing 
  // the oldest alarm indicated by arr_ai[#].alarm & arr_ai[#].alarm_last_ms.
  if (timerAiAlarmPublishLast > millis())  timerAiAlarmPublishLast = millis();
  if ((millis() - timerAiAlarmPublishLast) > TIMER_AI_ALARM_PUBLISH_INTERVAL_MS) {
    // Find the index for the oldest alarm, indicated by arr_ai[#].alarm & arr_ai[#].alarm_last_ms
    unsigned long oldest_ms = 0;
    byte oldest_alarm = 255;
    for (int i=0; i<AI_COUNT; i++) {
      if (arr_ai[i].alarm && millis() - arr_ai[i].alarm_last_ms > oldest_ms) {
        oldest_ms = millis() - arr_ai[i].alarm_last_ms;
        oldest_alarm = i;
      }
    }  // for
    // Publish the oldest alarm to the Particle Cloud
    if (oldest_alarm == 255) {
      Serial.printlnf("%u No ai alarms to publish", millis());
    } else {
      // Publish the oldest alarm
      if (PLATFORM_ID == PLATFORM_XENON) {
        if (arr_ai[oldest_alarm].fs_val < arr_ai[oldest_alarm].fs_low_limit) {
          Serial.printlnf("%u, %s LOW (%.4f < %.4f %s) for more than %u ms", millis(), arr_ai[oldest_alarm].pinName.c_str(), arr_ai[oldest_alarm].fs_val, arr_ai[oldest_alarm].fs_low_limit, arr_ai[oldest_alarm].fs_unit.c_str(), arr_ai[oldest_alarm].timer_limit_low_ms);
          arr_ai[oldest_alarm].timer_low_ms = millis();  
        } else if (arr_ai[oldest_alarm].fs_val > arr_ai[oldest_alarm].fs_high_limit) {
          Serial.printlnf("%u, %s HIGH (%.4f > %.4f %s) for more than %u ms", arr_ai[oldest_alarm].pinName.c_str(), arr_ai[oldest_alarm].fs_val, arr_ai[oldest_alarm].fs_high_limit, arr_ai[oldest_alarm].fs_unit.c_str(), arr_ai[oldest_alarm].timer_limit_high_ms);
          arr_ai[oldest_alarm].timer_high_ms = millis();  
        }
        arr_ai[oldest_alarm].alarm = false;
      } else {
        if (arr_ai[oldest_alarm].fs_val < arr_ai[oldest_alarm].fs_low_limit) {
          Serial.printlnf("%u, %s LOW (%.4f < %.4f %s) for more than %u ms", millis(), arr_ai[oldest_alarm].pinName.c_str(), arr_ai[oldest_alarm].fs_val, arr_ai[oldest_alarm].fs_low_limit, arr_ai[oldest_alarm].fs_unit.c_str(), arr_ai[oldest_alarm].timer_limit_low_ms);
          arr_ai[oldest_alarm].timer_low_ms = millis();  
          byte iPubResult = Particle.publish(arr_ai[oldest_alarm].pinName, String::format("LOW %.4f %s for more than %u ms", arr_ai[oldest_alarm].fs_val, arr_ai[oldest_alarm].fs_unit.c_str(), arr_ai[oldest_alarm].timer_limit_low_ms), PRIVATE);
          if (iPubResult == 1) arr_ai[oldest_alarm].alarm = false;
        } else if (arr_ai[oldest_alarm].fs_val > arr_ai[oldest_alarm].fs_high_limit) {
          Serial.printlnf("%s HIGH (%.4f > %.4f %s) for more than %u ms", arr_ai[oldest_alarm].pinName.c_str(), arr_ai[oldest_alarm].fs_val, arr_ai[oldest_alarm].fs_high_limit, arr_ai[oldest_alarm].fs_unit.c_str(), arr_ai[oldest_alarm].timer_limit_high_ms);
          arr_ai[oldest_alarm].timer_high_ms = millis();  
          byte iPubResult = Particle.publish(arr_ai[oldest_alarm].pinName, String::format("HIGH %.4f %s for more than %u ms", arr_ai[oldest_alarm].fs_val, arr_ai[oldest_alarm].fs_unit.c_str(), arr_ai[oldest_alarm].timer_limit_high_ms), PRIVATE);
          if (iPubResult == 1) arr_ai[oldest_alarm].alarm = false;
        }
      } // PLATFORM_ID
      arr_ai[oldest_alarm].alarm_last_ms = millis();
    } // oldest_alarm
    timerAiAlarmPublishLast = millis();
  } // timer
} // ProcessAiAlarms()

Single Channel / Variable Webhook


{
  "value": 1.35,
  "timestamp": 1590016063305,
  "context": {
    "lat": 40.44,
    "lng": -76.12
  }
}

 


Do you need help developing or customizing a IoT product for your needs?   Send me an email requesting a free one hour phone / web share consultation.  

 

The information presented on this website is for the author's use only.   Use of this information by anyone other than the author is offered as guidelines and non-professional advice only.   No liability is assumed by the author or this web site.