Bitcoin Price Index JSON Parse TCP Client

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 :sparkles: :sparkling_heart: :spark: :rainbow:

#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

#Web IDE file structure

6 Likes

That’s awesome! JSON parsing, Rest clients all on the core, nice! :slight_smile:

1 Like

Very nice, heaps of little goodies in there!

1 Like

This is great! Thanks for doing this. However, I am having such a hard time understanding this, not because the code is confusing, but because I do not really know that basics of header and .cpp files.

What is the best way I can learn about and learn to utilize these files? Are there any recommended tutorials that I can reference?
Thanks!

1 Like

@hassaan22, basically these are two Libraries that I’ve included in my BitCoin application. Each library consists of a Class source file (.cpp) and Class definition file (.h), also referred to as the Implementation and Declaration respectively.

It’s just a nice way to package up these functions to make them easier to use and extend, while at the same time compartmentalizing them which helps to protect certain functions and memory from being accessed accidentally.

Lots of resources on the web to help explain this:

http://playground.arduino.cc/Code/Library

http://www.tutorialspoint.com/cprogramming/c_header_files.htm

3 Likes