├── .gitignore ├── Readme.md ├── btc-ticker-esp8266.ino ├── docs └── images │ ├── btc-ticker-esp8266-matrix32.jpg │ ├── btc-ticker-esp8266.jpg │ └── photo_coinboard_case.jpg └── exchanges.h /.gitignore: -------------------------------------------------------------------------------- 1 | btc-ticker-esp8266.ino.d1_mini.bin 2 | *.bin -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Bitcoin price ticker with ESP8266 (realtime websockets) 2 | 3 | * 7-segment 8-bit - or - Dot Matrix LED Display support with 32x8px 4 | * bitstamp & bitfinex websocket interfacing for lightning fast, real time updates! 5 | * solderless build possible (if you order the display with pre-soldered pin headers) 6 | * low power (<0.5W), cheap to build (around 5 EUR) 7 | 8 | ## pictures in action 9 | ![animation](https://thumbs.gfycat.com/VainBeautifulAcornwoodpecker-size_restricted.gif) 10 | v0.2-bitstamp-websockets in action ([gfycat link](https://gfycat.com/gifs/detail/VainBeautifulAcornwoodpecker)) 11 | 12 | ![coinboard case](docs/images/photo_coinboard_case.jpg) 13 | 3D printed coinboard case 14 | 15 | ![picture](docs/images/btc-ticker-esp8266-matrix32.jpg) 16 | v0.3 with dot matrix display 17 | 18 | 19 | ![picture](docs/images/btc-ticker-esp8266.jpg) 20 | original prototype 21 | 22 | ## components 23 | * ESP8266 Wemos D1 R2 Uno ([link](http://s.click.aliexpress.com/e/cN7TWnfi)) or ESP8266s/NodeMCUs ([link](http://s.click.aliexpress.com/e/bqhV6bqg)) or Wemos D1 Mini 24 | * 7 segment display with MAX7219 ([link](http://s.click.aliexpress.com/e/7uottDW)) or Dot Matrix display ([link](http://s.click.aliexpress.com/e/Jckdk7Q)) 25 | * dupont cables (female-to-male) and maybe a cheap antenna ;) 26 | 27 | ## wiring 7-segment (software SPI, Wemos D1 Mini) 28 | 29 | D1 Mini | Display 30 | --- | --- 31 | GND | GND 32 | 5V/VIN | VCC 33 | D8 | CS 34 | D7 | DIN 35 | D5 | CLK 36 | 37 | ## wiring dot-matrix-display using hardware SPI 38 | 39 | NodeMCU | Display 40 | --- | --- 41 | GND | GND 42 | 5V/VIN | VCC 43 | D2 | CS 44 | D5 | CLK 45 | D7 | DIN 46 | 47 | ## how to install 48 | - flash the board 49 | * upload source sketch with arduino IDE 50 | * or flash binary [(download)](https://github.com/nebman/btc-ticker-esp8266/releases) with [esptool (python)](https://github.com/espressif/esptool) or [flash download tools (WIN)](https://espressif.com/en/support/download/other-tools) @ address 0x0 51 | - connect board to power 52 | - connect your smartphone/computer to ESPxxxxxx wifi 53 | - enter your home wifi settings at the captive portal 54 | 55 | ## 3D printed case options 56 | 57 | these are the 3rd party designs I have tested for this project 58 | 59 | Case | Link | Remarks 60 | -----|-----|----- 61 | coinboard|https://www.thingiverse.com/thing:2785082 |Wemos D1 only, works okish with some hotglue ([picture](docs/images/photo_coinboard_case.jpg)) 62 | ESP8266 Matrix Display Case|https://www.thingiverse.com/thing:2885225| printed too small, pay attention to your matrix module size! mine was a little larger 63 | IR Remote Tester|https://www.thingiverse.com/thing:1413083|works good (I closed the sensor hole above the display) but screw mechanism needs rework or maybe screw inserts 64 | 65 | ## known issues 66 | 67 | - compilation error in LedControl.h: 68 | solution: comment out or delete pgmspace.h include 69 | 70 | 71 | ## TODO 72 | 73 | * ! correct level-shifting to 5V (3.3V is out of spec, but works anyway) 74 | * ~~add 5th digit for next ATHs ;-)~~ 75 | * web portal configuration 76 | * ~~better error handling (although it seems to be pretty stable with good connection)~~ 77 | * ~~maybe use websockets for real-time ticker~~ 78 | * ~~add more APIs and currencies~~ plus option to choose 79 | * ~~TLS support for HTTPS requests~~ 80 | * ~~options for 3D printed case~~ 81 | 82 | -------------------------------------------------------------------------------- /btc-ticker-esp8266.ino: -------------------------------------------------------------------------------- 1 | #include "exchanges.h" 2 | 3 | // CONFIGURATION PART ************************************************************************* 4 | 5 | // Set DEBUGGING for addiditonal debugging output over serial 6 | // #define DEBUGGING 7 | 8 | // Set Display type, either SEGMENT7 or MATRIX32 9 | #define SEGMENT7 10 | //#define MATRIX32 11 | 12 | // Features MDNS, OTA 13 | #define FEATURE_OTA 14 | #define FEATURE_MDNS 15 | 16 | // set Hostname 17 | #define HOSTNAME "ESP-BTC-TICKER" 18 | 19 | #define ENABLE_BITFINEX // enable WSS support and array handling 20 | exchange_settings exchange = bitfinexUSDBTC; 21 | 22 | // END CONFIG ********************************************************************************* 23 | 24 | 25 | #ifdef FEATURE_MDNS 26 | #include 27 | #endif 28 | 29 | #ifdef FEATURE_OTA 30 | #include 31 | #endif 32 | 33 | #include 34 | 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include // https://github.com/pablorodiz/Arduino-Websocket 40 | 41 | #ifdef SEGMENT7 42 | #include 43 | #endif 44 | #ifdef MATRIX32 45 | #include 46 | #include 47 | #endif 48 | 49 | WiFiManager wifiManager; 50 | WiFiClientSecure client; 51 | WebSocketClient ws; 52 | 53 | // Variables and constants for timeouts 54 | 55 | const int timeout_hard_threshold = 120000; // reconnect after 120sec 56 | const int timeout_soft_threshold = 60000; // timeout default 60sec, send ping to check connection 57 | boolean timeout_soft_sent_ping = false; 58 | unsigned long timeout_next = 0; 59 | unsigned long timeout_flashing_dot = 0; 60 | unsigned int timeout_reconnect_count = 0; 61 | 62 | #ifdef SEGMENT7 63 | LedControl lc = LedControl(D7, D5, D8, 1); 64 | unsigned int timeout_swap_usdbtc = 0; 65 | boolean usdbtc = false; 66 | #endif 67 | #ifdef MATRIX32 68 | U8G2_MAX7219_32X8_F_4W_HW_SPI matrix(U8G2_R0, D2, U8X8_PIN_NONE); 69 | byte _progress = 0; 70 | String _text = ""; 71 | boolean _dot = false; 72 | #endif 73 | 74 | // current values 75 | int last = 0; 76 | int err = 0; 77 | 78 | void setup() { 79 | // start serial for debug output 80 | Serial.begin(115200); 81 | Serial.println(); 82 | Serial.println("INIT"); 83 | 84 | initDisplay(); 85 | clearDisplay(); 86 | writeStringDisplay("boot", true); 87 | 88 | // use 8 dots for startup animation 89 | setProgress(1); 90 | Serial.println("WIFI AUTO-CONFIG"); 91 | writeStringDisplay("Auto", true); 92 | 93 | // autoconfiguration portal for wifi settings 94 | #ifdef HOSTNAME 95 | WiFi.hostname(HOSTNAME); 96 | #endif 97 | wifiManager.setAPCallback(configModeCallback); 98 | wifiManager.autoConnect(); 99 | 100 | // wifi setup finished 101 | setProgress(2); 102 | Serial.println("WIFI DONE"); 103 | 104 | #ifdef FEATURE_MDNS 105 | // MDNS 106 | if (!MDNS.begin(HOSTNAME)) { 107 | Serial.println("MDNS ERROR!"); 108 | } else { 109 | MDNS.addService("http", "tcp", 80); 110 | Serial.println("mDNS UP"); 111 | } 112 | setProgress(3); 113 | #endif 114 | 115 | #ifdef FEATURE_OTA 116 | // Arduino OTA 117 | ArduinoOTA.onStart([]() { 118 | Serial.println("OTA Start"); 119 | writeStringDisplay("OTASTART", true); 120 | }); 121 | ArduinoOTA.onEnd([]() { 122 | Serial.println("OTA End"); 123 | writeStringDisplay("OTAEND", true); 124 | }); 125 | ArduinoOTA.begin(); 126 | setProgress(4); 127 | Serial.println("OTA started"); 128 | #endif 129 | 130 | setProgress(5); 131 | connect(false); 132 | } 133 | 134 | void connect(boolean reconnect) { 135 | timeout_soft_sent_ping = false; 136 | 137 | if (reconnect) { 138 | timeout_reconnect_count++; 139 | if (timeout_reconnect_count > 2) reboot(); 140 | clearDisplay(); 141 | } 142 | 143 | // connect to the server 144 | Serial.println("Client connecting..."); 145 | if (client.connect(exchange.host, exchange.port)) { 146 | Serial.println("WS Connected"); 147 | setProgress(6); 148 | } else { 149 | Serial.println("WS Connection failed."); 150 | reboot(); 151 | } 152 | 153 | // set up websocket details 154 | ws.setPath(exchange.url); 155 | ws.setHost(exchange.host); 156 | ws.setProtocol(exchange.wsproto); 157 | 158 | Serial.println("Starting WS handshake..."); 159 | 160 | if (ws.handshake(client)) { 161 | Serial.println("WS Handshake successful"); 162 | setProgress(7); 163 | } else { 164 | Serial.println("WS Handshake failed."); 165 | reboot(); 166 | } 167 | 168 | // subscribe to event channel "live_trades" 169 | Serial.println("WS Subscribe"); 170 | ws.sendData(exchange.subscribe); 171 | setProgress(8); 172 | 173 | 174 | 175 | // Finish setup, complete animation, set first timeout 176 | clearDisplay(); 177 | setProgress(0); 178 | timeout_next = millis() + timeout_hard_threshold; 179 | 180 | Serial.println("All set up, waiting for first trade..."); 181 | Serial.println(); 182 | } 183 | 184 | void loop() { 185 | #ifdef FEATURE_OTA 186 | ArduinoOTA.handle(); 187 | #endif 188 | ws.process(); // process websocket 189 | 190 | // check for hard timeout 191 | if( (long)(millis() - timeout_next) >= 0) { 192 | Serial.println(); 193 | Serial.println("TIMEOUT -> RECONNECT"); 194 | Serial.println(); 195 | connect(true); 196 | } 197 | 198 | // check for soft timeout (slow day?) send websocket ping 199 | if(!timeout_soft_sent_ping && client.connected() && (long)(millis() - timeout_next + timeout_soft_threshold) >= 0) { 200 | // ok, lets send a PING to check connection 201 | Serial.println("soft timeout -> sending ping to server"); 202 | ws.sendData("", WS_OPCODE_PING); 203 | timeout_soft_sent_ping = true; 204 | yield(); 205 | } 206 | 207 | // flash the dot when a trade occurs 208 | flashDotTrade(); 209 | 210 | // alternate currency display 211 | alternateCurrency(); 212 | 213 | // check if socket still connected 214 | if (client.connected()) { 215 | String line; 216 | uint8_t opcode = 0; 217 | 218 | if (ws.getData(line, &opcode)) { 219 | 220 | // check for PING packets, need to reply with PONG, else we get disconnected 221 | if (opcode == WS_OPCODE_PING) { 222 | Serial.println("GOT PING"); 223 | ws.sendData("{\"event\": \"pusher:pong\"}", WS_OPCODE_PONG); 224 | Serial.println("SENT PONG"); 225 | yield(); 226 | } else if (opcode == WS_OPCODE_PONG) { 227 | Serial.println("GOT PONG, connection still active"); 228 | timeout_soft_sent_ping = false; 229 | timeout_next = millis() + timeout_hard_threshold; 230 | } 231 | 232 | // check for data in received packet 233 | if (line.length() > 0) { 234 | #ifdef DEBUGGING 235 | Serial.print("Received data: "); 236 | Serial.println(line); 237 | #endif 238 | 239 | // parse JSON 240 | StaticJsonBuffer<768> jsonBuffer; 241 | JsonObject& root = jsonBuffer.parseObject(line); 242 | JsonArray& rootarray = jsonBuffer.parseArray(line); 243 | 244 | yield(); 245 | 246 | // alright, check for trade events 247 | if (root["event"] == "trade") { 248 | timeout_next = millis() + timeout_hard_threshold; 249 | Serial.print("GOT TRADE "); 250 | 251 | // need to deserialize twice, data field is escaped 252 | JsonObject& trade = jsonBuffer.parseObject(root["data"].as()); 253 | yield(); 254 | 255 | if (!trade.success()) { 256 | Serial.println("parse json failed"); 257 | return; 258 | } 259 | #ifdef DEBUGGING 260 | trade.printTo(Serial); 261 | #endif 262 | 263 | // extract price 264 | last = trade["price_str"]; // last USD btc 265 | 266 | Serial.print("price = "); 267 | Serial.println(last); 268 | 269 | } 270 | 271 | #ifdef ENABLE_BITFINEX 272 | else if (rootarray[1] == "tu") { // Bitfinex Tradeupdate 273 | last = rootarray[2][3]; 274 | Serial.print("price = "); 275 | Serial.println(last); 276 | timeout_next = millis() + timeout_hard_threshold; 277 | } else if (rootarray[1] == "hb") { // Bitfinex Heartbeat 278 | timeout_next = millis() + timeout_hard_threshold; 279 | } 280 | #endif 281 | 282 | else { // print unknown events and arrays 283 | root.printTo(Serial); 284 | rootarray.printTo(Serial); 285 | Serial.println(); 286 | } 287 | 288 | writePriceDisplay(true); 289 | } 290 | } 291 | } else { 292 | Serial.println("Client disconnected."); 293 | connect(true); 294 | } 295 | 296 | delay(5); 297 | } 298 | 299 | void writePriceDisplay(boolean flashDot) { 300 | #ifdef SEGMENT7 301 | // this is gentlemen. 302 | if (last >= 10000) { 303 | lc.setDigit(0, 4, (last/10000), false); 304 | } else { 305 | lc.setChar(0, 4, ' ',false); // 306 | } 307 | 308 | lc.setDigit(0, 3, (last%10000)/1000, false); 309 | lc.setDigit(0, 2, (last%1000)/100, false); 310 | lc.setDigit(0, 1, (last%100)/10, false); 311 | lc.setDigit(0, 0, (last%10), flashDot); 312 | #endif 313 | 314 | if (flashDot) { 315 | timeout_flashing_dot = millis() + 100; 316 | } 317 | 318 | #ifdef MATRIX32 319 | char val[8] = "$"; 320 | itoa(last, val+1, 10); 321 | flashDotTrade(); 322 | writeStringDisplay(val, true); 323 | #endif 324 | } 325 | 326 | #ifdef SEGMENT7 327 | void setAll(char c, boolean dot = false, int from = 0, int len = 4); 328 | void setAll(char c, boolean dot, int from, int len) { 329 | for (int i=from; igetConfigPortalSSID(), true); 356 | Serial.println(myWiFiManager->getConfigPortalSSID()); 357 | } 358 | 359 | /* 360 | * display segment bits 361 | * 362 | * 1 363 | * ----- 364 | * | | 365 | * 6 | | 2 366 | * | | 367 | * ----- 368 | * | 7 | 369 | * 5 | | 3 370 | * | | 371 | * ----- . 0 372 | * 4 373 | * 374 | */ 375 | 376 | void writeStringDisplay(String s, boolean fillEmpty = false); 377 | void writeStringDisplay(String s, boolean fillEmpty) { 378 | #ifdef SEGMENT7 379 | int len = s.length(); 380 | if (len > 8) len = 8; 381 | char c; 382 | 383 | for (byte i=0; i 0) matrix.drawLine(0, 7, (_progress*4)-1, 7); 449 | if (_dot) matrix.drawPixel(31,7); 450 | } while ( matrix.nextPage() ); 451 | } 452 | #endif 453 | 454 | void initDisplay() { 455 | #ifdef SEGMENT7 456 | // wakeup 7 segment display 457 | lc.shutdown(0, false); 458 | // set brightness 459 | lc.setIntensity(0, 6); 460 | #endif 461 | #ifdef MATRIX32 462 | matrix.begin(); 463 | matrix.setContrast(70); 464 | #endif 465 | } 466 | 467 | void clearDisplay() { 468 | #ifdef SEGMENT7 469 | // clear all digits 470 | setAll(' ', false, 0, 8); 471 | #endif 472 | #ifdef MATRIX32 473 | matrix.clear(); 474 | #endif 475 | } 476 | 477 | void setProgress(byte progress) { 478 | #ifdef SEGMENT7 479 | // set first dot... etc 480 | for (byte i=0; i<8; i++) { 481 | lc.setLed(0, 7-i, 0, i= 0)) { 494 | flashdot = false; 495 | } else { 496 | flashdot = true; 497 | } 498 | 499 | #ifdef SEGMENT7 500 | lc.setLed(0, 0, 0, flashdot); 501 | #endif 502 | #ifdef MATRIX32 503 | if (_dot != flashdot) { 504 | _dot = flashdot; 505 | redrawDisplay(); 506 | } 507 | #endif 508 | } 509 | 510 | void alternateCurrency() { 511 | #ifdef SEGMENT7 512 | // alternate USD and BTC in display every 10sec 513 | if (((long)(millis() - timeout_swap_usdbtc) >= 0)) { 514 | if (usdbtc) { 515 | writeStringDisplay("USD"); 516 | } else { 517 | writeStringDisplay("BTC"); 518 | } 519 | usdbtc = !usdbtc; 520 | timeout_swap_usdbtc = millis() + 10000; 521 | } 522 | #endif 523 | } 524 | -------------------------------------------------------------------------------- /docs/images/btc-ticker-esp8266-matrix32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nebman/btc-ticker-esp8266/2b90e6ef67cb61eb7ba5cf2a17845018702b55db/docs/images/btc-ticker-esp8266-matrix32.jpg -------------------------------------------------------------------------------- /docs/images/btc-ticker-esp8266.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nebman/btc-ticker-esp8266/2b90e6ef67cb61eb7ba5cf2a17845018702b55db/docs/images/btc-ticker-esp8266.jpg -------------------------------------------------------------------------------- /docs/images/photo_coinboard_case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nebman/btc-ticker-esp8266/2b90e6ef67cb61eb7ba5cf2a17845018702b55db/docs/images/photo_coinboard_case.jpg -------------------------------------------------------------------------------- /exchanges.h: -------------------------------------------------------------------------------- 1 | 2 | struct exchange_settings { 3 | char* host; 4 | int port; 5 | char* url; 6 | char* wsproto; 7 | char* subscribe; 8 | }; 9 | 10 | // bitstamp USD BTC tested & working 11 | 12 | const exchange_settings bitstampUSDBTC = { 13 | "ws.pusherapp.com", 14 | 80, 15 | "/app/de504dc5763aeef9ff52?protocol=7", 16 | "pusher", 17 | "{\"event\": \"pusher:subscribe\", \"data\": {\"channel\": \"live_trades\"}}" 18 | }; 19 | 20 | // UNTESTED 21 | 22 | const exchange_settings bitstampEURBTC = { 23 | "ws.pusherapp.com", 24 | 80, 25 | "/app/de504dc5763aeef9ff52", 26 | "pusher", 27 | "{\"event\": \"pusher:subscribe\", \"data\": {\"channel\": \"live_trades_btceur\"}}" 28 | }; 29 | 30 | // UNTESTED 31 | 32 | const exchange_settings gdaxUSDBTC = { 33 | "ws-feed-public.sandbox.gdax.com", 34 | 80, 35 | "/", 36 | "websocket", 37 | "{\"type\": \"subscribe\", \"channels\": [{\"name\": \"ticker\",\"product_ids\": [\"BTC-USD\"]}]}" 38 | }; 39 | 40 | // new UNTESTED 41 | 42 | const exchange_settings bitfinexUSDBTC = { 43 | "api.bitfinex.com", 44 | 443, 45 | "/ws/2", 46 | "websocket", 47 | "{\"event\":\"subscribe\",\"channel\":\"trades\",\"symbol\":\"tBTCUSD\" }" 48 | }; 49 | 50 | --------------------------------------------------------------------------------