Particle Projects

May 2020

Particle Particle Webhook to Thinger.io


Thinger.io is a Open-Source cloud IoT Platform.   Data is stored in buckets, and endpoints are used to execute a service such as sending a SMS, email, or calling a REST API.  

Devices, dashboards, buckets, endpoints, and file systems can be organized into a Project.   The Project Manager can then be used to assign users, and in that way organize devices to a particular customer.  

Thinger.io also supports branding.   A unique Thinger.io instance can be assigned to a particular brand, and in this way multiple brands can be created, each with a unique list of projects / customers, users, and devices.  

As of May 2020, cloud hosted and fully managed instances are available on Amazon Web Services and DigitalOcean (Google Cloud and Microsoft Azure are offered for On-premise licenses only).  

Configure Thinger.io

Create a free account and login.   In the console, go to the Devices menu and add a new device, giving it a name relevent to your Particle device.   Don't bother with the Credentials.   Click the Add Device button.  

Click on the console left toolbar option Access Tokens and then click on Add Token.   We are going to create a token that can only be used by a Particle device to send data to Thinger.io.   Name the Token ID something like 'particle_devicew' and give it a Token Name such as 'Particle device write to Thinger'.   Under the Allow - Permissions section, click on the Add button and in the Select Permission Type dropdown box choose Device, select 'Any Device' under the Access section, and then under the Actions section choose 'Select specific action' and the option CallDeviceCallBack.   Click on the Update Token to save your changes and copy the Access Token at the bottom of the windows to a text editor for later (Access Token name is "particle_device_w").  

In order to store data received by Thinger.io, you need to create a "bucket".   Click on the Data Buckets on the console left tool bar and then Add Bucket.   Fill out the form, making sure to enable the bucket and to select the Data Source as 'From Device Write Call' option.  

Then go back to the Device page, choose your device, click on the Callback menu option, and under the Settings tab, click the Write Bucket checkbox and choose the bucket you just created.   Click the Save button.  

Continuing on the Callback page, click on the Overview tab, and copy the URL under Method to your text editor ("device callback URL").   Ignore the Authorization Header shown on the form.  

Use an API Tool such as Insomnia and post data to Thinger.io.   Build a JSON body in your API tool as shown below and then try to send a POST.  


{
"temperature": 78.3,
"humidity": 33.42
}

Click on the device list to see the Last Connection for the device to confirm that data sent was received by Thinger.io.   In the Thinger.io console, click on Data Buckets and click on the Bucket you just created.   In the Bucket Data section you should see the data you have just sent (click Refresh button at the bottom to refresh).   Once you have a "200 OK" response, you are ready to build your Particle Webhook.  

Create a Particle Webhook for Thinger.io

We will push two or more analog channel values from our Particle device to the Particle Cloud with a unique event name of "thinger_io".   The Webhook will see this event, and then forward the data to Thinger.io.  

In the Particle console, click on the Integrations link on the left toolbar.   Click on NEW INTEGRATION and choose the Webhook option.   Enter "thinger_io" for the Event Name, and fill out the form as shown below.   Replace the URL with the "device callback URL".   In the HTTP HEADERS section of the form, add a new row with the key value of 'Authorization' and then to the right the value should be your "particle_device_w" that you copied to your text editor, with "Bearer " concatenated to the beginning as shown in the image below.  

Install and run the following code on your Particle device:


#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);
      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("thinger_io", 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()

The code above sends the following JSON string to the Particle Cloud every 15 seconds (values 1.61 and 2.00 vary based on the voltage at the analog inputs, of course).  


{"A1":1.61,"A2":2.00}

After your device is running and sending data to the Particle Webhook, visit the Thinger.io console, click on the device list to see the Last Connection for the device to confirm that data sent was received by Thinger.io.   In the Thinger.io console, click on Data Buckets and click on the Bucket you just created.   In the Bucket Data section you should see the data you have just sent (click Refresh button at the bottom to refresh).   Once you have data in your bucket you are ready to go to the Thinger.io Dashboards and create a dashboard to visualize your data.  

Thinger Dashboards

In the Thinger.io console, click on the left toolbar icon labeled Dashboards, and then the Add Dashboard button.   Fill out the form, entering a Dashboard id, Dashboard name, and Dashboard description of your choosing.   Then click the Add Dashboard button.   At the top right, click on the slider control to enable it.  

Click on the Add Widget button and fill out the form as shown below.  

Then configure the Time Series Chart as shown below and click the Save button.  

A line chart is then displayed as shown below.   Note that Thinger.io has already resolved the date/time stamps from the bucket for you.  

Try some of the other Widgets and have some fun!

 


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.