Last active
June 27, 2020 08:21
-
-
Save guo14/b6dcbc20c0fa15cddd25e4383d089a85 to your computer and use it in GitHub Desktop.
ESP32 Quotes TDA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* This package does the following: | |
* - connects to home WiFi, | |
* - set clock, | |
* - setup a websocket connection with TD Ameritrade backend, | |
* - login and subscribe to quotes of some tickers | |
*/ | |
#include <heltec.h> | |
#include <ArduinoJson.h> | |
#include <ArduinoWebsockets.h> | |
#include <EasyButton.h> | |
#include <WiFi.h> | |
#include <WiFiMulti.h> | |
#include <WiFiClientSecure.h> | |
#include <HTTPClient.h> | |
// Arduino pin where the button is connected to. | |
#define BUTTON_PIN 0 | |
using namespace websockets; | |
const uint8_t* small_font_size = ArialMT_Plain_10; | |
const uint8_t* medium_font_size = ArialMT_Plain_16; | |
const uint8_t* large_font_size = ArialMT_Plain_24; | |
const char* websockets_server = "wss://streamer-ws.tdameritrade.com/ws"; | |
WebsocketsClient ws_client; | |
EasyButton button(BUTTON_PIN); | |
typedef struct { | |
const char* wifi_ssid; | |
const char* wifi_password; | |
const char* login_req; // e.g. "{\"requests\":[{\"service\":\"ADMIN\",\"command\":\"LOGIN\",\"requestid\":0,\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"credential\":\"YOUR_CREDENTIAL\",\"qoslevel\":0,\"token\":\"YOUR_TOKEN\",\"version\":\"1.0\"}}]}"; | |
const char* quote_req; // e.g. "{\"requests\":[{\"service\":\"LEVELONE_FUTURES\",\"requestid\":1,\"command\":\"SUBS\",\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"keys\":\"/ES,/ZN,/GC\",\"fields\":\"0,3,19,20\"}}]}"; | |
} Config; | |
Config config; | |
class Quote { | |
String ticker; | |
float last_price; | |
float net_change; | |
float change_pct; | |
public: | |
Quote (String ticker); | |
void Update(float last_price, float net_change, float change_pct) { | |
this->last_price = last_price; | |
this->net_change = net_change; | |
this->change_pct = change_pct; | |
} | |
String RenderPriceStr() { | |
return ticker + " " + String(last_price, 2); | |
} | |
String RenderNetChangeStr() { | |
return (net_change > 0 ? "+" : " ") + String(net_change, 2); | |
} | |
String RenderChangePctStr() { | |
return (change_pct > 0 ? "+" : " ") + String(change_pct * 100.0, 2) + '%'; | |
} | |
}; | |
Quote::Quote(String ticker) { | |
this->ticker = ticker; | |
} | |
Quote es ("ES"); | |
Quote zn = Quote("ZN"); | |
Quote gc = Quote("GC"); | |
void InitDisplay() { | |
// Initialize the Heltec ESP32 object | |
Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Disable*/, true /*Serial Enable*/, true /*PABOOST Enable*/, 470E6 /**/); | |
Heltec.display -> clear(); | |
} | |
//void InitConfig() { | |
// config.wifi_ssid = ""; | |
// config.wifi_password = ""; | |
// // See https://developer.tdameritrade.com/content/streaming-data for details. | |
// config.login_req = \ | |
// "{\"requests\":[{\"service\":\"ADMIN\",\"command\":\"LOGIN\",\"requestid\":0,\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"credential\":\"YOUR_CREDENTIAL\",\"qoslevel\":0,\"token\":\"YOUR_TOKEN\",\"version\":\"1.0\"}}]}"; | |
// config.quote_req = \ | |
// "{\"requests\":[{\"service\":\"LEVELONE_FUTURES\",\"requestid\":1,\"command\":\"SUBS\",\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"keys\":\"/ES,/ZN,/GC\",\"fields\":\"0,3,19,20\"}}]}"; | |
//} | |
void InitWiFi() { | |
WiFi.mode(WIFI_STA); | |
WiFi.setAutoConnect(true); | |
WiFi.begin(config.wifi_ssid, config.wifi_password); | |
while(WiFi.status() != WL_CONNECTED) { | |
Serial.println("Connecting WiFi..."); | |
delay(500); | |
} | |
Heltec.display -> clear(); | |
} | |
void ParseData(JsonArray& json_array) { | |
for (int i = 0; i < json_array.size(); i++) { | |
Quote* quote_to_update; | |
const char* key = json_array[i]["key"]; | |
String ticker = String(key); | |
// find ticker to update | |
if (ticker == "/ES") { | |
quote_to_update = &es; | |
} else if (ticker == "/ZN") { | |
quote_to_update = &zn; | |
} else if (ticker == "/GC") { | |
quote_to_update = &gc; | |
} else { | |
Serial.print("Unknown ticker: "); | |
Serial.println(ticker); | |
continue; | |
} | |
// For other fields see https://developer.tdameritrade.com/content/streaming-data#_Toc504640604 | |
quote_to_update->Update(json_array[i]["3"], json_array[i]["19"], json_array[i]["20"]); | |
} | |
} | |
void OnMessageCallback(WebsocketsMessage message) { | |
Serial.print("Got Message: "); | |
Serial.println(message.data()); | |
StaticJsonDocument<2000> doc; | |
DeserializationError error = deserializeJson(doc, message.data()); | |
// Test if parsing succeeds. | |
if (error) { | |
Serial.print(F("deserializeJson() failed: ")); | |
Serial.println(error.c_str()); | |
return; | |
} | |
if (!doc["data"].isNull()) { // Incoming Data | |
JsonObject data_0 = doc["data"][0]; | |
JsonArray json_array = data_0["content"]; // complier need some type hints to disambiguate overloads | |
ParseData(json_array); | |
PrintScreen(); | |
} else if (!doc["notify"].isNull()) { // Incoming Heartbeat, ignore | |
Serial.println("Received heartbeat."); | |
} else if (!doc["response"].isNull()) { // Incoming Command Response | |
const char* cmd = doc["response"][0]["command"]; | |
long status_code = doc["response"][0]["content"]["code"]; | |
if (status_code != 0) { | |
Serial.println("Error in status_code, abort."); | |
} | |
if (strcmp(cmd, "LOGIN") == 0) { | |
Serial.println("Logged in TD Ameritrade."); | |
ws_client.send(config.quote_req); | |
} else if (strcmp(cmd, "SUBS") == 0) { | |
Serial.println("Subscribed quote stream."); | |
} else { | |
Serial.println("Unknown response."); | |
} | |
} else { | |
Serial.println("Unknown incoming doc."); | |
} | |
} | |
void OnEventsCallback(WebsocketsEvent event, String data) { | |
if(event == WebsocketsEvent::ConnectionOpened) { | |
Serial.println("Connnection Opened"); | |
ws_client.send(config.login_req); | |
} else if(event == WebsocketsEvent::ConnectionClosed) { | |
Serial.println("Connnection Closed"); | |
// TODO: print error, delay, then retry | |
} else if(event == WebsocketsEvent::GotPing) { | |
Serial.println("Got a Ping!"); | |
ws_client.pong(); | |
} else if(event == WebsocketsEvent::GotPong) { | |
Serial.println("Got a Pong!"); | |
} | |
} | |
void InitWebSocket() { | |
// Setup Callbacks | |
ws_client.onMessage(OnMessageCallback); | |
ws_client.onEvent(OnEventsCallback); | |
// Connect to server | |
ws_client.connect(websockets_server); | |
} | |
void PrintScreen() { | |
Heltec.display -> clear(); | |
Heltec.display -> setFont(large_font_size); | |
Heltec.display -> drawString(0, 0, es.RenderPriceStr()); | |
Heltec.display -> setFont(medium_font_size); | |
Heltec.display -> drawString(0, 24, es.RenderNetChangeStr() + " " + es.RenderChangePctStr()); | |
Heltec.display -> drawHorizontalLine(0, 40, 128); | |
Heltec.display -> setFont(small_font_size); | |
Heltec.display -> drawString(0, 41, String(zn.RenderPriceStr() + " " + zn.RenderNetChangeStr() + " " + zn.RenderChangePctStr())); | |
Heltec.display -> drawHorizontalLine(0, 52, 128); | |
Heltec.display -> drawString(0, 52, String(gc.RenderPriceStr() + " " + gc.RenderNetChangeStr() + " " + gc.RenderChangePctStr())); | |
Heltec.display -> display(); | |
} | |
uint8_t brightness = -1; | |
// Callback function to be called when the button is pressed. | |
void OnPressed() { | |
brightness -= 63; | |
Serial.print("Brightness changed to: "); | |
Serial.println(brightness); | |
Heltec.display -> setBrightness(brightness); | |
} | |
// the setup routine runs once when starts up | |
void setup() { | |
Serial.begin(115200); | |
InitConfig(); | |
InitDisplay(); | |
InitWiFi(); | |
InitWebSocket(); | |
// Initialize the button. | |
button.begin(); | |
// Add the callback function to be called when the button is pressed. | |
button.onPressed(OnPressed); | |
} | |
// the loop routine runs over and over again forever | |
void loop() { | |
ws_client.poll(); | |
button.read(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment