Example: Logging and graphing data from your Spark Core using Google

If I put:

curl https://api.particle.io/v1/devices/xxxxx/freshCount \
     -d access_token=xxxxx \
     -d "args=freshCount"

into Terminal, it resets count as I would like it to do. It does not work when in my google script as:

var url = 'https://api.particle.io/v1/devices/xxxxx/freshCount \
     -d access_token=xxxxx \
     -d "args=freshCount"'

or

var url = 'curl https://api.particle.io/v1/devices/xxxxx/freshCount \
     -d access_token=xxxxx \
     -d "args=freshCount"'

I have tried to put:
https://api.particle.io/v1/devices/xxxxx/freshCount?access_token=xxxx/"args=freshCount"
and
https://api.particle.io/v1/devices/xxxxx/freshCount/access_token=xxxx/"args=freshCount"
into my browser window (firefox) and get the following error (although I know my token is correct).

{
  "error": "invalid_token",
  "error_description": "The access token provided is invalid."
}

Anyway, that’s my update.

Alright, the first function has correct syntax and thus should/does work. The two following ones have an invalid CURL syntax and will thus not work, nothing unexpected so far.

Yup, that sounds about right. Browser make GET requests when you enter a URL. That's what's being used for variables, since you're GETting them. When calling functions, you need a POST request, which is something the browser won't do for you, thus the requests fail. Again, nothing unexpected.

That also partially explains why your google script isn't working.
I'm going to assume that the first URL call is successful in retrieving the result, but the function then doesn't get called?
First of all, you're not doing anything with the function URL, you're just assigning it to a variable, that's it. If you want to use it, you'll most likely have to use that UrlFetchApp.fetch thing again.
Secondly, there's no indication you're trying to do a POST request, meaning that it'll probably do a GET request, and thus fail. Looking at the example code which I linked to, you might have more success if you try to follow that a bit more closely. Pasting it here for easy reference, and so you know which one I'm talking about:

var payload = 'XXXXX'; // Replace with raw video data.
var headers = {
    'GData-Version': '2',
    'Slug': 'dog-skateboarding.mpeg'
    // Add any other required parameters for the YouTube API.
};
var url = 'https://uploads.gdata.youtube.com/feeds/api/users/default/uploads';
var options = {
    'method': 'post',
    'headers': headers,
    'payload': payload
};
var response = UrlFetchApp.fetch(url, options);

You'll most likely have to put the accesstoken in the body, which I'm guessing is the 'payload' here, but I'm not sure on that one.

Mind you, I haven't yet used Google's script options yet, so this is merely an educated guess.

Gotcha! Thanks for all the help anyway. This is going to take someone like me a bit to figure out.

I came across this page in the link you sent. These are some cuts from it:


// This sample sends POST payload data in the style of an HTML form, including
 // a file.

 function sendHttpPost() {

   // Download a file now (GET), so we can upload it in the HTTP POST below.
   var response = UrlFetchApp.fetch("http://example.com/image_to_download.jpg");
   var fileBlob = response.getBlob();

   var payload =
   {
     "fieldOne" : "value for field one",
     "fieldTwo" : "value for field two",
     "fileAttachment": fileBlob
   };

   // Because payload is a JavaScript object, it will be interpreted as
   // an HTML form. (We do not need to specify contentType; it will
   // automatically default to either 'application/x-www-form-urlencoded'
   // or 'multipart/form-data')

   var options =
   {
     "method" : "post",
     "payload" : payload
   };

   UrlFetchApp.fetch("http://example.com/upload_form.cgi", options);
 }

And these:

headers - Object - a JavaScript key/value map of HTTP headers for the request

method - String - the HTTP method for the request: 'post', 'get', 'put',
'delete', etc. The default is 'get'.

payload - String - the payload (e.g. POST body) for the request. Certain HTTP
methods (e.g. GET) do not have any payload. It can be a String, a byte array, or a
JavaScript key/value map. See the examples for more detail.

I'm currently trying to decide if having the payload is even necessary. This is what I'm working w now. I've just added the new function in the Google script, after the collectData function.

function collectData() {
  var  sheet = SpreadsheetApp.getActiveSheet();

  var response = UrlFetchApp.fetch("https://api.spark.io/v1/devices/xxxxx/result?access_token=xxxxx");
  
  try {
    var response = JSON.parse(response.getContentText()); // parse the JSON the Core API created
    
    var result = unescape(response.result); // you'll need to unescape before your parse as JSON
  
    try {
      var p = JSON.parse(result); // parse the JSON you created
      var d = new Date(); // time stamps are always good when taking readings
      sheet.appendRow([d, p.count]); // append the date, count to the sheet      
    } 
    catch(e)
    {
      Logger.log("Unable to do second parse");
    }
  } catch(e)
  {
    Logger.log("Unable to returned JSON");
  }

}
function sendHttpPost() {
  var url = 'https://api.particle.io/v1/devices/xxxxx/freshCount \
     -d access_token=xxxxx \
     -d "args=freshCount"';
      var options = {
        'method': 'post'
      };
      UrlFetchApp.fetch(url, options);
}

I think this is getting closer??? We need someone with a bit more Google Script to chime in. :grinning:

I also tried with the curl before "https...". No go. The way that address is formatted a problem? Im buessing the " \ " at the end of the line and the " -d " preceding the next line just says its continuous? If I were to make it one line, would it look like:

https://api.particle.io/v1/devices/xxxx/freshCount?access_token=xxxxx"args=freshCount" ?

Even matter?

Again, thanks for the help. I'll keep hammering away.

Alright, you’re getting closer.

Again, first things first, you can’t mix the CURL stuff with the script stuff. CURL is just a tool you use to call those URLs. The -d is a special command, belonging to CURL, and thus cannot be used in the script. The \ are also CURL related if I’m not mistaken. Long story short, ditch CURL, you don’t need it, and should only be used for testing purposes. I think Postman might be more convenient, since it’s a bit more user-friendly, and will give you a better understanding of what’s happening.

So, in regards to the formatting of the URL; Try following the example they’ve created, but replace the values with your own. Most important is the options field. The method should be post as explained earlier. And as the excerpt you have about the payload mentions, it contains the body for a POST call. Following the Particle docs here, that’s what’s needed for the access token and your arguments.

Give it a shot, and see how far you get :smile:

CURL, you’re dead to me.

So, I have been using this.

function collectData() {

//Blah Blah, collect count and post it to my sheet
}

function sendHttpPost() {
  var url = 'https://api.spark.io/v1/devices/xxxxx/arg="freshCount"/access_token=xxxxx';
  var options = {
    'method': 'post'
  };
      UrlFetchApp.fetch(url, options);
}

And when I run the function collectData(), it works with no errors. If I run just the function sendHttpPost(), I get the following error.

Invalid argument: 
https://api.spark.io/v1/devices/xxxxx/arg="freshCount"/access_token=xxxxx
 (line 31, file "")

I dunno. I’ll keep at it but it might be time for a break.

Your arguments and accesstoken need to be in the payload, or the POST body, like I mentioned above, not in the URL :wink:

1 Like

Boom!

It appears it is working! Dude, thanks for all the help! I just ditched the extra function sendHttpPost() and put the POST command into the other function loop and tried to format that payload area correctly but at first glance, it appears we are in business. I’ll report back otherwise.

For anybody that comes across this and wants a counter that will log to Google, here is my code and Google script that resets the counter after each data parse. And, I’m sure it’s not the cleanest and I have a few counting things to work on (thanks @ScruffR for the interrupt homework).

int Gate = D4;
int Led = D7;
int gateState = 0;
int count = 0;
int newCount(String command);
char resultstr[64];

void setup() {

  pinMode(Gate, INPUT);
  pinMode(Led, OUTPUT);
  Particle.variable("result", resultstr, STRING); 
  Particle.function("freshCount", newCount);
}

void loop() {
    
  static byte prevState = 1;
  gateState = digitalRead(Gate);
  digitalWrite(Led, LOW);
  
  if(gateState != prevState) 
  {
    if(gateState == HIGH)
    {
    count ++;
    digitalWrite(Led, HIGH);
    }
    prevState = gateState; 
  } 
    sprintf(resultstr, "{\"count\" :%d}", count);
    delay(50);
}
int newCount(String command)
{
  if(command == "freshCount")
  {
    count = 0;
  }
}

 

Google Script

function collectData() {
  var  sheet = SpreadsheetApp.getActiveSheet();

  var response = UrlFetchApp.fetch("https://api.spark.io/v1/devices/DEVICE_ID/result?access_token=xxxxxx");
  
  try {
    var response = JSON.parse(response.getContentText()); // parse the JSON the Core API created
    
    var result = unescape(response.result); // you'll need to unescape before your parse as JSON
  
    try {
      var p = JSON.parse(result); // parse the JSON you created
      var d = new Date(); // time stamps are always good when taking readings
      sheet.appendRow([d, p.count]); // append the date, count to the sheet       
    } 
    catch(e)
    {
      Logger.log("Unable to do second parse");
    }
  } catch(e)
  {
    Logger.log("Unable to returned JSON");
  }
  
  var url = 'https://api.spark.io/v1/devices/DEVICE_ID/freshCount';
  var payload =
          {
            "access_token" : "xxxxxx",
            "arg" : "freshCount"
          };
  var options = 
          {
            'method' : 'post',
            'payload' : payload
          };
  
  UrlFetchApp.fetch(url, options);
      
}

:grinning:

3 Likes

Hi, aguspg,

I just left a similar message to an old thread on Ubidots community. Perhaps you can help me here in case that thread is dead.

I cannot get my Particle Photon to send data to Ubidots. I am using the sample project from Ubidots that reads a photocell. I added the HTTPclient library to the code (per instructions), but get several error messages. For one, it says my “variable is empty”. I am using the proper Ubidots Token and not my API Key. When I check variable details it simply says:
“detail”: "Authentication credentials were not provided."
Why is the server denying my request to send data to Ubidots? What should I check to find out where the problem is?
Thanks,

@ScruffR - Not even sure where to put this but wanted to show you what I have so far. Homework! Unfortunately, I don’t have my photon with me. Again, I’ve decided to go back to posting to Ubidots since Google script was unreliable at pulling the count (but, very reliable at resetting my counter!).

SYSTEM_MODE(MANUAL);
#include "HttpClient/HttpClient.h"
#include "SparkFunMAX17043/SparkFunMAX17043.h"
#define VARIABLE_ID "variable_id"
#define VARIABLE_ID2 "variable_id2"
#define TOKEN "my_token"


int Gate = D5;
int Led = D4;
int Led2 = D2;
int sendDelay = 3600000;
int count = 0;
double soc = 0;
unsigned long previousMillis = 0;
HttpClient http;

http_header_t headers[] = {
      { "Content-Type", "application/json" },
      { "X-Auth-Token" , TOKEN },
    { NULL, NULL } 
};

http_request_t request;
http_response_t response;

void setup() {

  pinMode(Gate, INPUT_PULLDOWN);
  attachInterrupt(Gate, bubbleCounting, RISING);
  pinMode(Led, OUTPUT);
  pinMode(Led2, OUTPUT);
  lipo.begin(); 
  lipo.quickStart();
  digitalWrite(Led, HIGH);
  delay(500);
  digitalWrite(Led, LOW);
  delay(200);
  digitalWrite(Led, HIGH);
  delay(500);
  digitalWrite(Led, LOW);
  delay(200);
  digitalWrite(Led, HIGH);
  delay(1000);
  digitalWrite(Led, LOW);
  delay(200);
 
}

void bubbleCounting (void) {

    count ++;
    digitalWrite(Led2, HIGH);
    delay(20);
    digitalWrite(Led2, LOW);
}

void loop() {
 
  unsigned long currentMillis = millis();
  
  if(currentMillis - previousMillis >= sendDelay)
    {
    WiFi.on();
    WiFi.connect();
    soc = lipo.getSOC();
    waitUntil(WiFi.ready);
    request.port = 80;
    request.hostname = "things.ubidots.com";
    request.path = "/api/v1.6/variables/"VARIABLE_ID"/values";
    request.body = "{\"value\":" + String(count) + "}";
    http.post(request, response, headers);
    request.path = "/api/v1.6/variables/"VARIABLE_ID2"/values";
    request.body = "{\"value\":" + String(soc) + "}";
    http.post(request, response, headers);
    previousMillis = currentMillis;
    count = 0;
    digitalWrite(Led, HIGH);
    delay(20);
    digitalWrite(Led, LOW);
    WiFi.off();
    }
  delay(100);
}

Look remotely close?

@1eb13, I can maybe help you with that if you want to post what you’re working with.

[quote]
void bubbleCounting (void) {
    count ++;
    digitalWrite(Led2, HIGH);
    delay(20);
    digitalWrite(Led2, LOW);
}

Not too bad :wink:

You just should declare your counter like this

volatile int count;  // to make sure that the code optimizer does not get in your way

And the most important bit don’t ever use delay() inside an ISR!

If you want the blinking you can set a flag in your ISR and do the blinking in loop().

So change declaration of int count = 0; to volatile int count = 0; ?

Who is this “code optimizer” and why would he get in my way?

What about


void bubbleCounting (void) {
    count ++;
    digitalWrite(Led2, HIGH);
    digitalWrite(Led2, LOW);
}

I just want some super brief signal that there is counting taking place.

Excited to try this out.

And another question about Interrupts: Seems like they take priority over whatever is taking place in the loop, correct? What if my code is in the loop and trying to connect to server to upload current count and a new bubble hits. Will that abort the establishing of connection to upload? Or will it allow it to finish through that part of the loop? Should I add noInterrupts(); at the begging of my upload code and then interrupts(); after to avoid that scenario?

Yes.

This is a part of the compiler that looks at your code and tries to improve it for speed or/and size.
And hence it might happen when it looks at a part of your code where you use count multiple times it will optimize this part in a way where it only retrieves it once from memory and uses it over and over again, ignoring any change made by the interrupt. volatile tells it to always retrieve the current value from mem.

Your blink will be unobservably fast the way you do it there.
If you do this, you'll get a LED toggle with each bubble

void bubbleCounting (void) {
    count++;
    digitalWriteFast(Led2, !pinReadFast(Led2)); // toggle led
}

Correct, hence the need to keep them short and fast so the upload won't even notice.
So also no need to mask the interrupts since it will return and carry on just where it left off before the interrupt.
But in order to avoid missing a count, you might want to do count = 0; immediately after this line request.body = "{\"value\":" + String(count) + "}";

1 Like

When I do this, the light stays on until the next bubble toggles via RISING edge. I'm worried that will leave the bulb on too long and drain the battery. Is there any other way to just blink the bulb very quick without using a delay?

I also have tested this and it seems to work good. I'll get this light blink thing figured, charge it up and see if using the interrupt method adds to battery life.

Sure, this was my initial suggestion

1 Like

To make a light blink you'll have to turn it on, wait, and turn it off. Considering a human blinks in about 200ms, you might want to consider how long you'd like the light (thus the wait/delay) to be on. If you can figure out a way to keep your light on momentarily, without causing issues with the ISR, then you should be good.
Setting a flag, and handling that in your main loop is likely the best option.


@ScruffR is too fast, yet again :frowning:

2 Likes

Thanks fellas,

I know what you already suggested but before I saw that I found in the docs where it said “Since delay() requires interrupts to work, it will not work if called inside an ISR. Using delayMicroseconds() will work as normal.”

So I tried


void bubbleCounting (void) {

    count ++;
    digitalWriteFast(Led2, HIGH);
    delayMicroseconds(50);
    digitalWriteFast(Led2, LOW);
}

Its pretty quick and faint, but you can see when it counts and it seems to work. Do you see a problem with doing it this way?

I guess you don’t want to hear what we are saying :confused:

50”s is not a lot but very superfluous and useless in an ISR!

1 Like

@brybrew, I agree with @ScruffR and @Moors7. Setting a flag in the ISR (make sure it is declared volatile) and reading it in you loop() will allow you to blink the LED for any duration of time without ISR interference. You may not see EVERY bubble (if they are fast) via a blink but I don’t believe that’s the point, is it?

Any delay in an ISR, even 50us, is considerable when it is wasted, non-code running time. :wink: