Here’s a full example of a Bitcoin Price Index JSON parsing TCP Client. The end result is the BPI value as a Double precision variable that you can work with. Remember you have to open a serial terminal after you boot your Core, then press ENTER. Successive presses of ENTER will perform another GET request. Modify to suit your own needs for total world domination!
Note: This is a unsecure HTTP request… do NOT use this code to buy or sell bitcoins.
Keep it fluffy people
#BitcoinPrice.ino
/*
* Bitcoin Price Index JSON Parsing TCP Client Example
* BDub @ Technobly.com 6/27/2014
* LICENSE: MIT (C) 2014 BDub
*
*/
#pragma SPARK_NO_PREPROCESSOR
#include "application.h"
#include "rest_client.h"
#include "jsmnSpark.h"
#define TOKEN_STRING(js, t, s) \
(strncmp(js+(t).start, s, (t).end - (t).start) == 0 \
&& strlen(s) == (t).end - (t).start)
#define TOKEN_PRINT(t) \
Serial.print(", type: "); Serial.print((t).type); Serial.print(" size: "); Serial.print((t).size); \
Serial.print(" start: "); Serial.print((t).start); Serial.print(" end: "); Serial.println((t).end)
/* IMPORTANT TO CHANGE THE NUMBER OF TOKENS TO MATCH YOUR DATA CLOSELY */
#define NUM_TOKENS 23
#define MAX_OBJ_SIZE 50
#define HOSTNAME "api.coindesk.com"
RestClient client = RestClient(HOSTNAME);
String response;
void setup() {
Serial.begin(9600); // Make sure serial terminal is closed before powering up Core
while(!Serial.available()) SPARK_WLAN_Loop(); // Open serial terminal now, and press ENTER
}
void loop() {
int i, r;
jsmn_parser p;
jsmntok_t tok[NUM_TOKENS];
char obj[MAX_OBJ_SIZE];
// Press ENTER in your serial terminal to continue...
if (!Serial.available())
return;
obj[0] = Serial.read(); // Flush the serial buffer to pause next time through
response = ""; // Clear the response String
// GET request
int statusCode = client.get("/v1/bpi/currentprice/USD.json", &response);
// Uncomment following line to force a test response, real response will not have all of these escaped characters
//response = "{\"time\":{\"updated\":\"Jun 27, 2014 04:17:00 UTC\",\"updatedISO\":\"2014-06-27T04:17:00+00:00\",\"updateduk\":\"Jun 27, 2014 at 05:17 BST\"},\"disclaimer\":\"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org\",\"bpi\":{\"USD\":{\"code\":\"USD\",\"rate\":\"577.6150\",\"description\":\"United States Dollar\",\"rate_float\":577.615}}}";
if(statusCode != 200) {
Serial.print("Error code from server: ");
Serial.println(statusCode);
return;
}
Serial.print("Response body from server: ");
Serial.println(response);
Serial.println(" ");
// Parse response from server
jsmn_init(&p);
r = jsmn_parse(&p, response.c_str(), tok, NUM_TOKENS);
// Determine status code
if (r == JSMN_SUCCESS) {
Serial.println("Parsed successfully.");
}
else if(r == JSMN_ERROR_INVAL) {
Serial.println("Bad token, JSON string is corrupted!");
return;
}
else if(r == JSMN_ERROR_NOMEM) {
Serial.println("Not enough tokens, JSON string is too large! Increase NUM_TOKENS.");
return;
}
else if(r == JSMN_ERROR_PART) {
Serial.println("JSON string is too short, expecting more JSON data!");
return;
}
else {
Serial.println("Parse failed! Unknown Error.");
return;
}
// Print out a list of Tokens
for (i = 0; i < NUM_TOKENS; i++) {
Serial.print("Token ");
Serial.print(i);
TOKEN_PRINT(tok[i]);
delay(10);
}
// Convert 17th token to string
i = 17;
strlcpy(obj, &response.c_str()[tok[i].start], (tok[i].end - tok[i].start + 1));
Serial.print("\nToken["); Serial.print(i); Serial.print("]: ");
Serial.println(obj); // Print it out now just in case it's not the right one,
// we'll get and idea of where we are in the object
// Does this token == "rate" ?
if ( TOKEN_STRING(response.c_str(), tok[i], "rate") )
{
// Convert next token to string
i++;
strlcpy(obj, &response.c_str()[tok[i].start], (tok[i].end - tok[i].start + 1));
// Convert string to double, contains numerical value of Bitcoin Price Index
double bpi = strtod(obj, NULL);
// Print double out to 4 decimal places
Serial.print("Token["); Serial.print(i); Serial.print("]: ");
Serial.println(bpi, 4);
// Take control of the RGB LED
RGB.control(true);
// Change the RGB's color to GREEN
RGB.color(0, 255, 0);
// Delay for one second
delay(1000);
// Release control of the RGB LED
RGB.control(false);
}
else {
Serial.println("'rate' token not found");
}
}
#jsmnSpark.cpp
#include "application.h"
#include "jsmnSpark.h"
/**
* Allocates a fresh unused token from the token pull.
*/
static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser,
jsmntok_t *tokens, size_t num_tokens) {
jsmntok_t *tok;
if (parser->toknext >= num_tokens) {
return NULL;
}
tok = &tokens[parser->toknext++];
tok->start = tok->end = -1;
tok->size = 0;
#ifdef JSMN_PARENT_LINKS
tok->parent = -1;
#endif
return tok;
}
/**
* Fills token type and boundaries.
*/
static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type,
int start, int end) {
token->type = type;
token->start = start;
token->end = end;
token->size = 0;
}
/**
* Fills next available token with JSON primitive.
*/
static jsmnerr_t jsmn_parse_primitive(jsmn_parser *parser, const char *js,
jsmntok_t *tokens, size_t num_tokens) {
jsmntok_t *token;
int start;
start = parser->pos;
for (; js[parser->pos] != '\0'; parser->pos++) {
switch (js[parser->pos]) {
#ifndef JSMN_STRICT
/* In strict mode primitive must be followed by "," or "}" or "]" */
case ':':
#endif
case '\t' : case '\r' : case '\n' : case ' ' :
case ',' : case ']' : case '}' :
goto found;
}
if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
#ifdef JSMN_STRICT
/* In strict mode primitive must be followed by a comma/object/array */
parser->pos = start;
return JSMN_ERROR_PART;
#endif
found:
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
parser->pos--;
return JSMN_SUCCESS;
}
/**
* Filsl next token with JSON string.
*/
static jsmnerr_t jsmn_parse_string(jsmn_parser *parser, const char *js,
jsmntok_t *tokens, size_t num_tokens) {
jsmntok_t *token;
int start = parser->pos;
parser->pos++;
// Skip starting quote
for (; js[parser->pos] != '\0'; parser->pos++) {
char c = js[parser->pos];
// Quote: end of string
if (c == '\"') {
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
return JSMN_SUCCESS;
}
// Backslash: Quoted symbol expected
if (c == '\\') {
parser->pos++;
switch (js[parser->pos]) {
// Allowed escaped symbols
case '\"': case '/' : case '\\' : case 'b' :
case 'f' : case 'r' : case 'n' : case 't' :
break;
// Allows escaped symbol \uXXXX
case 'u':
parser->pos++;
//int i = 0;
for(int i = 0; i < 4 && js[parser->pos] != '\0'; i++) {
// If it isn't a hex character we have an error
if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || // 0-9
(js[parser->pos] >= 65 && js[parser->pos] <= 70) || // A-F
(js[parser->pos] >= 97 && js[parser->pos] <= 102))) { // a-f
parser->pos = start;
return JSMN_ERROR_INVAL;
}
parser->pos++;
}
parser->pos--;
break;
// Unexpected symbol
default:
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
}
parser->pos = start;
return JSMN_ERROR_PART;
}
/**
* Parse JSON string and fill tokens.
*/
jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, jsmntok_t *tokens,
unsigned int num_tokens) {
jsmnerr_t r;
int i;
jsmntok_t *token;
for (; js[parser->pos] != '\0'; parser->pos++) {
char c;
jsmntype_t type;
c = js[parser->pos];
switch (c) {
case '{': case '[':
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL)
return JSMN_ERROR_NOMEM;
if (parser->toksuper != -1) {
tokens[parser->toksuper].size++;
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
}
token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
token->start = parser->pos;
parser->toksuper = parser->toknext - 1;
break;
case '}': case ']':
type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
#ifdef JSMN_PARENT_LINKS
if (parser->toknext < 1) {
return JSMN_ERROR_INVAL;
}
token = &tokens[parser->toknext - 1];
for (;;) {
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
token->end = parser->pos + 1;
parser->toksuper = token->parent;
break;
}
if (token->parent == -1) {
break;
}
token = &tokens[token->parent];
}
#else
for (i = parser->toknext - 1; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
parser->toksuper = -1;
token->end = parser->pos + 1;
break;
}
}
/* Error if unmatched closing bracket */
if (i == -1) return JSMN_ERROR_INVAL;
for (; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
parser->toksuper = i;
break;
}
}
#endif
break;
case '\"':
r = jsmn_parse_string(parser, js, tokens, num_tokens);
if (r < 0) return r;
if (parser->toksuper != -1)
tokens[parser->toksuper].size++;
break;
case '\t' : case '\r' : case '\n' : case ':' : case ',': case ' ':
break;
#ifdef JSMN_STRICT
/* In strict mode primitives are: numbers and booleans */
case '-': case '0': case '1' : case '2': case '3' : case '4':
case '5': case '6': case '7' : case '8': case '9':
case 't': case 'f': case 'n' :
#else
/* In non-strict mode every unquoted value is a primitive */
default:
#endif
r = jsmn_parse_primitive(parser, js, tokens, num_tokens);
if (r < 0) return r;
if (parser->toksuper != -1)
tokens[parser->toksuper].size++;
break;
#ifdef JSMN_STRICT
/* Unexpected char in strict mode */
default:
return JSMN_ERROR_INVAL;
#endif
}
}
for (i = parser->toknext - 1; i >= 0; i--) {
/* Unmatched opened object or array */
if (tokens[i].start != -1 && tokens[i].end == -1) {
return JSMN_ERROR_PART;
}
}
return JSMN_SUCCESS;
}
/**
* Creates a new parser based over a given buffer with an array of tokens
* available.
*/
void jsmn_init(jsmn_parser *parser) {
parser->pos = 0;
parser->toknext = 0;
parser->toksuper = -1;
}
#jsmnSpark.h
#ifndef __JSMN_H_
#define __JSMN_H_
/**
* JSON type identifier. Basic types are:
* o Object
* o Array
* o String
* o Other primitive: number, boolean (true/false) or null
*/
typedef enum {
JSMN_PRIMITIVE = 0,
JSMN_OBJECT = 1,
JSMN_ARRAY = 2,
JSMN_STRING = 3
} jsmntype_t;
typedef enum {
/* Not enough tokens were provided */
JSMN_ERROR_NOMEM = -1,
/* Invalid character inside JSON string */
JSMN_ERROR_INVAL = -2,
/* The string is not a full JSON packet, more bytes expected */
JSMN_ERROR_PART = -3,
/* Everything was fine */
JSMN_SUCCESS = 0
} jsmnerr_t;
/**
* JSON token description.
* @param type type (object, array, string etc.)
* @param start start position in JSON data string
* @param end end position in JSON data string
*/
typedef struct {
jsmntype_t type;
int start;
int end;
int size;
#ifdef JSMN_PARENT_LINKS
int parent;
#endif
} jsmntok_t;
/**
* JSON parser. Contains an array of token blocks available. Also stores
* the string being parsed now and current position in that string
*/
typedef struct {
unsigned int pos; /* offset in the JSON string */
unsigned int toknext; /* next token to allocate */
int toksuper; /* superior token node, e.g parent object or array */
} jsmn_parser;
/**
* Create JSON parser over an array of tokens
*/
void jsmn_init(jsmn_parser *parser);
/**
* Run JSON parser. It parses a JSON data string into and array of tokens, each describing
* a single JSON object.
*/
jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js,
jsmntok_t *tokens, unsigned int num_tokens);
#endif __JSMN_H_
#rest_client.cpp
/**
******************************************************************************
* @file rest_client.cpp
*
* details: https://github.com/llad/spark-restclient
*
* credit: https://github.com/csquared/arduino-restclient
*
******************************************************************************
*/
#include "rest_client.h"
//#define HTTP_DEBUG
#ifdef HTTP_DEBUG
#define HTTP_DEBUG_PRINT(string) (Serial.print(string))
#endif
#ifndef HTTP_DEBUG
#define HTTP_DEBUG_PRINT(string)
#endif
RestClient::RestClient(const char* _host){
host = _host;
port = 80;
num_headers = 0;
contentTypeSet = false;
}
RestClient::RestClient(const char* _host, int _port){
host = _host;
port = _port;
num_headers = 0;
contentTypeSet = false;
}
// GET path
int RestClient::get(const char* path){
return request("GET", path, NULL, NULL);
}
//GET path with response
int RestClient::get(const char* path, String* response){
return request("GET", path, NULL, response);
}
// POST path and body
int RestClient::post(const char* path, const char* body){
return request("POST", path, body, NULL);
}
// POST path and body with response
int RestClient::post(const char* path, const char* body, String* response){
return request("POST", path, body, response);
}
// PUT path and body
int RestClient::put(const char* path, const char* body){
return request("PUT", path, body, NULL);
}
// PUT path and body with response
int RestClient::put(const char* path, const char* body, String* response){
return request("PUT", path, body, response);
}
// DELETE path
int RestClient::del(const char* path){
return request("DELETE", path, NULL, NULL);
}
// DELETE path and response
int RestClient::del(const char* path, String* response){
return request("DELETE", path, NULL, response);
}
// DELETE path and body
int RestClient::del(const char* path, const char* body ){
return request("DELETE", path, body, NULL);
}
// DELETE path and body with response
int RestClient::del(const char* path, const char* body, String* response){
return request("DELETE", path, body, response);
}
void RestClient::write(const char* string){
HTTP_DEBUG_PRINT(string);
client.print(string);
}
void RestClient::setHeader(const char* header){
headers[num_headers] = header;
num_headers++;
}
// The mother- generic request method.
//
int RestClient::request(const char* method, const char* path,
const char* body, String* response){
HTTP_DEBUG_PRINT("HTTP: connect\n");
if(client.connect(host, port)){
HTTP_DEBUG_PRINT("HTTP: connected\n");
HTTP_DEBUG_PRINT("REQUEST: \n");
// Make a HTTP request line:
write(method);
write(" ");
write(path);
write(" HTTP/1.0\r\n");
for(int i=0; i<num_headers; i++){
write(headers[i]);
write("\r\n");
}
write("Host: ");
write(host);
write("\r\n");
write("Connection: close\r\n");
if(body != NULL){
char contentLength[30];
sprintf(contentLength, "Content-Length: %d\r\n", strlen(body));
write(contentLength);
if(!contentTypeSet){
write("Content-Type: application/x-www-form-urlencoded\r\n");
}
}
write("\r\n");
if(body != NULL){
write(body);
write("\r\n");
write("\r\n");
}
//make sure you write all those bytes.
delay(100);
HTTP_DEBUG_PRINT("HTTP: call readResponse\n");
int statusCode = readResponse(response);
HTTP_DEBUG_PRINT("HTTP: return readResponse\n");
//cleanup
HTTP_DEBUG_PRINT("HTTP: stop client\n");
num_headers = 0;
client.stop();
delay(50);
HTTP_DEBUG_PRINT("HTTP: client stopped\n");
return statusCode;
}else{
HTTP_DEBUG_PRINT("HTTP Connection failed\n");
return 0;
}
}
int RestClient::readResponse(String* response) {
// an http request ends with a blank line
boolean currentLineIsBlank = true;
boolean httpBody = false;
boolean inStatus = false;
char statusCode[4];
int i = 0;
int code = 0;
if(response == NULL){
HTTP_DEBUG_PRINT("HTTP: NULL RESPONSE POINTER: \n");
}else{
HTTP_DEBUG_PRINT("HTTP: NON-NULL RESPONSE POINTER: \n");
}
HTTP_DEBUG_PRINT("HTTP: RESPONSE: \n");
while (client.connected()) {
HTTP_DEBUG_PRINT(".");
if (client.available()) {
HTTP_DEBUG_PRINT(",");
char c = client.read();
HTTP_DEBUG_PRINT(c);
if(c == ' ' && !inStatus){
inStatus = true;
}
if(inStatus && i < 3 && c != ' '){
statusCode[i] = c;
i++;
}
if(i == 3){
statusCode[i] = '\0';
code = atoi(statusCode);
}
//only write response if its not null
if(httpBody){
if(response != NULL) response->concat(c);
}
if (c == '\n' && httpBody){
HTTP_DEBUG_PRINT("HTTP: return readResponse2\n");
return code;
}
if (c == '\n' && currentLineIsBlank) {
httpBody = true;
}
if (c == '\n') {
// you're starting a new lineu
currentLineIsBlank = true;
}
else if (c != '\r') {
// you've gotten a character on the current line
currentLineIsBlank = false;
}
}
}
HTTP_DEBUG_PRINT("HTTP: return readResponse3\n");
return code;
}
#rest_client.h
/**
******************************************************************************
* @file rest_client.h
*
* details: https://github.com/llad/spark-restclient
*
* credit: https://github.com/csquared/arduino-restclient
*
******************************************************************************
*/
#include "application.h"
class RestClient {
public:
RestClient(const char* host);
RestClient(const char* _host, int _port);
//Client Setup
void dhcp();
int begin(byte*);
//Generic HTTP Request
int request(const char* method, const char* path,
const char* body, String* response);
// Set a Request Header
void setHeader(const char*);
// GET path
int get(const char*);
// GET path and response
int get(const char*, String*);
// POST path and body
int post(const char* path, const char* body);
// POST path and body and response
int post(const char* path, const char* body, String*);
// PUT path and body
int put(const char* path, const char* body);
// PUT path and body and response
int put(const char* path, const char* body, String*);
// DELETE path
int del(const char*);
// DELETE path and body
int del(const char*, const char*);
// DELETE path and response
int del(const char*, String*);
// DELETE path and body and response
int del(const char*, const char*, String*);
private:
TCPClient client;
int readResponse(String*);
void write(const char*);
const char* host;
int port;
int num_headers;
const char* headers[10];
boolean contentTypeSet;
};
#sample output
Response body from server: {"time":{"updated":"Jun 27, 2014 05:07:00 UTC","updatedISO":"2014-06-27T05:07:00+00:00","updateduk":"Jun 27, 2014 at 06:07 BST"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","bpi":{"USD":{"code":"USD","rate":"580.6150","description":"United States Dollar","rate_float":580.615}}}
Parsed successfully.
Token 0, type: 1 size: 6 start: 0 end: 405
Token 1, type: 3 size: 0 start: 2 end: 6
Token 2, type: 1 size: 6 start: 8 end: 128
Token 3, type: 3 size: 0 start: 10 end: 17
Token 4, type: 3 size: 0 start: 20 end: 45
Token 5, type: 3 size: 0 start: 48 end: 58
Token 6, type: 3 size: 0 start: 61 end: 86
Token 7, type: 3 size: 0 start: 89 end: 98
Token 8, type: 3 size: 0 start: 101 end: 126
Token 9, type: 3 size: 0 start: 130 end: 140
Token 10, type: 3 size: 0 start: 143 end: 298
Token 11, type: 3 size: 0 start: 301 end: 304
Token 12, type: 1 size: 2 start: 306 end: 404
Token 13, type: 3 size: 0 start: 308 end: 311
Token 14, type: 1 size: 8 start: 313 end: 403
Token 15, type: 3 size: 0 start: 315 end: 319
Token 16, type: 3 size: 0 start: 322 end: 325
Token 17, type: 3 size: 0 start: 328 end: 332
Token 18, type: 3 size: 0 start: 335 end: 343
Token 19, type: 3 size: 0 start: 346 end: 357
Token 20, type: 3 size: 0 start: 360 end: 380
Token 21, type: 3 size: 0 start: 383 end: 393
Token 22, type: 0 size: 0 start: 395 end: 402
Token[17]: rate
Token[18]: 580.6150