LED Bitcoin DashboardThis is a topic dedicated to my latest little Bitcoin project!
This is a 64x64 (P3) RGB LED matrix panel that can be bought for roughly $20 to $30. I'm sure they are sold for much less when used in their
intended application and bought in bulk, to form the large billboards that we know and love. They indeed often consist of tons of these panels chained together.
This project utilizes a microcontroller and libraries that do support chaining, though I've yet to try it. This would allow to cram more data onto this 'dashboard'.
PartsLED MatrixOf course, the main component is the LED matrix. They can be acquired for roughly $20-30 or equivalent in other currencies.
I would recommend a 64x64; the pitch (P) shouldn't really matter, but I got a tighter P3 pitch so it looks sharper instead of having huge gaps between individual LEDs. That would give you a larger end result, though if that's what you're into.
Microcontroller and matrix shieldI got one of these shields before the shop recently closed, paired with a D1 Mini ESP32, since it's much better than an ESP8266 in a few ways, like multithreading.
https://www.tindie.com/products/brianlough/esp32-matrix-shield-mini-32/Similar products and microcontrollers should work, though they will require small changes to the code.
Power SupplyThe whole thing is powered by a 5V barrel jack power supply. The amperage / wattage is kind of up to you; especially in such a text-based application, it doesn't have to be very high since not a lot of LEDs are on at the same time. Mine with 10A is on the higher side (one of the highest ratings I could find at 5V that are still in a power-brick package and not 'LED power supply style' - nowadays known for their usage in 3D printers). It can illuminate the whole panel, so you'll be good with half of that for this project.
The matrix shield powers the matrix as well as the microcontroller.
InformationAt the moment, it shows:
- Block height
- Bitcoin mined
- Fastest fee rate
- USD per BTC
- sat per USD
- Time based on time zone
As we spoke about this project in another thread, I got a few suggestions on how to improve it, and this is my current idea for adding more data, especially about Lightning, without adding more displays.
I'm glad about any feedback on this as I believe cycling through screens will make it annoying, and I don't think all that data is needed very often. I do check it a few times a day as it is now and it's already pretty handy honestly.
I believe I'll split it up into multiple screens then:
Blockchain Data
- Block height
- Bitcoin mined
- Blockchain size
Mining
- Blocks until next diff
- Blocks until next halving
- Hashrate
Mempool
- Size
- Fee fastest
- Fee medium
- Fee slow
Price
- USD per coin
- [insert other currency] per coin
- sat per USD
- sat per [insert other currency]
Lightning
- Capacity
- Number of channels
- ...
It would become tricky to set a useful speed though; if it's too fast, you can't read it in time and it is distracting. If it's too slow, you can miss milestones like the 19M BTC (it was fun to watch it live!)..
CodeOf course, I'm sharing my code here. It's nothing special, Arduino-IDE based, just using a few libraries and calling some APIs. What's a little bit nifty though is the usage of the ESP32's secondary core to poll the APIs without interfering with the clock loop, so that the clock is updated independently from the internet speed.
I'm also not too fond of having to use 'Arduino Strings' here instead of
std::string, but the latter don't seem to work on Arduino, so it is what it is.
If you use the same hardware, you'll only really have to change
#define MYTIMEZONE "..." (to your time zone)
char ssid[] = "..."; (to your WiFi SSID)
char password[] = "..."; (to your WiFi password)
Other microcontrollers might just need a change of
#define P_OE 2 and possibly change of the multithreading / background thread part.
If your matrix is different (less LEDs), you'll want to set its size in
PxMATRIX display(64, 64, ...); correctly and of course need to remove a few lines of text to display on it.
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// https://github.com/2dom/PxMatrix
#include <PxMatrix.h>
// Adafruit GFX library is a dependancy for the PxMatrix Library
// Can be installed from the library manager
// https://github.com/adafruit/Adafruit-GFX-Library
// Library used for getting the time and adjusting for DST
// Search for "ezTime" in the Arduino Library manager
// https://github.com/ropg/ezTime
#include <ezTime.h>
// ---- Stuff to configure ----
// Initialize Wifi connection to the router
char ssid[] = "..."; // your network SSID (name)
char password[] = "..."; // your network key
// Set a timezone using the following list
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#define MYTIMEZONE "..."
// -----------------------------
// ----- Wiring -------
#define P_LAT 22
#define P_A 19
#define P_B 23
#define P_C 18
#define P_D 5
#define P_E 15
#define P_OE 2 // Generic ESP32
// ---------------------
struct NetworkData {
String height, btccount, price, sats, fees;
bool refresh;
} networkdata;
portMUX_TYPE displayMux = portMUX_INITIALIZER_UNLOCKED;
hw_timer_t* displayTimer = NULL;
TaskHandle_t dataRefreshTask;
PxMATRIX display(64, 64, P_LAT, P_OE, P_A, P_B, P_C, P_D, P_E);
Timezone myTZ;
// Set up the LED Bitcoin Dashboard
void setup() {
Serial.begin(115200);
// Attempt to connect to Wifi network:
Serial.print("Connecting Wifi: ");
Serial.println(ssid);
// Set WiFi to station mode and disconnect from an AP if it was Previously
// connected
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
// Do not set up display before WiFi connection as it will crash!
// Intialise display
display.begin(32);
display.setRotate(true);
display.clearDisplay();
display.setTextColor(display.color565(0, 255, 255));
display.setCursor(10,10);
startDisplayTimer(true);
yield();
display.clearDisplay();
display.print("BITCOIN");
display.showBuffer();
setDebug(INFO);
waitForSync();
Serial.println();
Serial.println("UTC: " + UTC.dateTime());
myTZ.setLocation(F(MYTIMEZONE));
Serial.print(F("Time in your set timezone: "));
Serial.println(myTZ.dateTime());
display.clearDisplay();
startBackgroundTask();
}
// Update the screen once per second
unsigned long timeDue = 0;
void loop() {
unsigned long now = millis();
if (now > timeDue) {
updateDisplay();
timeDue = now + 1000;
}
}
// -- Display refresh is necessary --
uint8_t displayDrawTime=6;
void IRAM_ATTR displayUpdater(){
// Increment the counter and set the time of ISR
portENTER_CRITICAL_ISR(&displayMux);
display.display(displayDrawTime);
portEXIT_CRITICAL_ISR(&displayMux);
}
void startDisplayTimer(bool is_enable) {
if (is_enable) {
displayTimer = timerBegin(0, 80, true);
timerAttachInterrupt(displayTimer, &displayUpdater, true);
timerAlarmWrite(displayTimer, 2000, true);
timerAlarmEnable(displayTimer);
}
else {
timerDetachInterrupt(displayTimer);
timerAlarmDisable(displayTimer);
}
}
// -- Data is refreshed on parallel core 0
void startBackgroundTask() {
xTaskCreatePinnedToCore(refreshData, "refreshData", 10000, NULL, 0, &dataRefreshTask, 0);
}
void refreshData(void* parameter) {
unsigned long timeDue = 0;
while(true) {
unsigned long now = millis();
if (now > timeDue) {
networkdata.price = getPrice();
networkdata.height = getBlockHeight();
networkdata.btccount = getBTCCount(networkdata.height);
networkdata.fees = getFees();
networkdata.sats = getSatsPerUSD(networkdata.price);
networkdata.refresh = true;
timeDue = now + 30000; // update every 30s
}
delay(2); // give CPU time for other tasks
}
}
// -- Begin Fetch Data --
String getBTCCount(String blockHeight) {
unsigned int height = blockHeight.toInt();
unsigned int btcCount = 210000*50 + 210000*25 + 210000*12.5 + (height-630000)*6.25;
String btcCountStr = String(btcCount);
return btcCountStr.substring(0,2) + "," + btcCountStr.substring(2,5) + "," + btcCountStr.substring(5,8);
}
String getPrice() {
String price = "";
HTTPClient http;
http.useHTTP10(true);
http.begin("https://api.hitbtc.com/api/2/public/ticker/btcusd");
if (http.GET()) {
DynamicJsonDocument doc(2048);
deserializeJson(doc, http.getStream());
price = doc["last"].as<String>();
}
http.end();
return price;
}
String getBlockHeight() {
HTTPClient http;
http.begin("https://mempool.space/api/blocks/tip/height");
if (http.GET()) {
String payload = http.getString();
http.end();
return payload;
}
else {
return "";
}
}
String getFees() {
String fees = " ";
HTTPClient http;
http.begin("https://mempool.space/api/v1/fees/recommended");
if (http.GET()) {
DynamicJsonDocument doc(2048);
deserializeJson(doc, http.getStream());
fees += doc["fastestFee"].as<String>();
}
return fees.substring(fees.length()-2);
}
String getSatsPerUSD(String price) {
unsigned int _price = price.toInt();
unsigned int sats = 100000000/_price;
return String(sats);
}
// -- End Fetch Data --
// -- Updating display contents --
void writeToScreen(uint8_t x, uint8_t y, String text) {
display.setCursor(x,y);
display.print(text);
}
String prevTimeString;
void updateDisplay() {
Serial.println("Update display");
String timeString = "";
// g = 12-hour format of hour, no leading zeros
// h = 12-hour format of hour with leading zeros
// i = minutes with leading zeros
timeString = myTZ.dateTime("h:i");
//If the length is only 4, pad it with space at the beginning
if (timeString.length() == 4) { timeString = " " + timeString; }
if (networkdata.refresh == true) {
for (int i=0; i<64; i++) for (int j=0; j<64-8; j++) display.drawPixel(i, j, 0);
writeToScreen(4, 0*8+2, networkdata.height+" blk");
writeToScreen(3, 1*8+2, networkdata.btccount);
writeToScreen(15, 2*8+2, networkdata.fees+"sat/vB");
writeToScreen(9, 4*8+1, networkdata.price+"$");
writeToScreen(9, 5*8+1, networkdata.sats+"sat/$");
networkdata.refresh = false;
}
if (timeString != prevTimeString) {
prevTimeString = timeString;
for (int i=0; i<64; i++) for (int j=63; j>63-8; j--) display.drawPixel(i, j, 0);
writeToScreen(33, 7*8+1, timeString);
}
}