pull down to refresh

Introduction

This tutorial describes how you can create your own version of a Blockclock1 showing the statistics of your own node and services including forwarded lightning payments, found mining shares by the Bitaxe etc.
The Bitclock consists of two parts:
  • Client: the IoT device showing the messages and
  • Server: that collects the information from your own node.
Prerequisite:
  • Access to your own node running on GNU/Linux. Optionally also lnd, datum gateway.
  • Microcontroller M5StickC Plus2
  • Basic GNU/Linux command line and administrative skills
Known Issues:
  • Because of the memory restriction the basic configuration of the M5StickC Plus2 does not include fonts supporting all Unicode code points as for instance ₿, ⚡.

Client

In this tutorial a M5StickC Plus2 is used a client. This device is using a ESP32 microcontroller including Wifi adapter and contains a 1.14" LCD display with a resolution of 135 x 240 pixel.
Programming this device is done with the Arduino IDE2 using a sketch. There are many tutorials available how to set up Arduino. Important is that you also add support for the IoT device you are using.
The sketch for the Bitclock looks like this:
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>

const char* SSID     = "<SSID>";
const char* PASSWORD = "<WIFI PASSWORD>";

const char* CLIENT_NAME = "bitclock";

const char* URL = "<SERVER IP><SERVER WEB PORT>/current";

const uint32_t REFRESH = 30000;

constexpr uint8_t FONT = 3;

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(FONT);
  WiFi.setHostname(CLIENT_NAME);
  WiFi.begin(SSID, PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(100);
}

void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(URL);
    int code = http.GET();
    String payload;
    if (code > 0) payload = http.getString();
    drawWrapped(payload);
    http.end();
  }
  delay(REFRESH);
}

void drawWrapped(const String& txt) {
  M5.Lcd.fillScreen(BLACK);
  int16_t maxWidth = M5.Lcd.width() - 4;
  std::vector<String> lines;
  String cur;
  int16_t lastSpaceIdx = -1;
  for (int i = 0; i < txt.length(); ++i) {
    cur += txt[i];
    if (txt[i]==' ') {
      lastSpaceIdx = i;
    }
    if (M5.Lcd.textWidth(cur.c_str()) > maxWidth) {
      if (txt[i] == ' ' || lastSpaceIdx == -1 ){
        cur.remove(cur.length() - 1);
        lines.push_back(cur);
        cur = txt[i];
       } else {
        cur.remove(cur.length() - (i - lastSpaceIdx + 1));
        lines.push_back(cur);
        cur = txt.substring(lastSpaceIdx, i + 1);
      }
      lastSpaceIdx = -1;
    }
  }
  if (cur.length()) lines.push_back(cur);
  int16_t totalH = lines.size() * M5.Lcd.fontHeight();
  int16_t y = (M5.Lcd.height() - totalH) / 2;
  for (auto& ln : lines) {
    int16_t x = (M5.Lcd.width() - M5.Lcd.textWidth(ln.c_str())) / 2;
    M5.Lcd.setCursor(x, y);
    M5.Lcd.print(ln);
    y += M5.Lcd.fontHeight();
  }
}
This program connects to the backend every 30 seconds and always fetches the same web page named current. Please replace the placeholder <XXX> with your specific data. Most of the logic in this program is concerned with handling nice line wrappring and centering the text on the device. Plesae mind that these devices may require an older Wifi standard to properly work. As does the Bitaxe3.

Server

The server part is realized entirly with bash scripts, systemd timers and tools available on any GNU/Linux distro following the KISS4 principle.
The setup is using three directories:
├── ctrl-script
├── data
└── data-script
The parent of these directories will be referenced as <BASE_PATH>. ctrl-script contains the logic to rotate the current information to be displayed. The data-script contains one or more scripts gathering one piece of information. Each of these scripts saves the result of its gathering into a file in the data directory.

Webserver

As web server we use in this tutorial busybox http that has to be configured with the following systemd service:
[Unit]
Description=busybox httpd server for bitclock
After=network.target

[Service]
User=bitclock
ExecStart=/usr/bin/busybox httpd -p <SERVER WEB PORT> -h <BASE_PATH>/data
Type=forking
Restart=on-failure
ProtectSystem=full
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
Make sure that <SERVER WEB PORT> is the same as in Arduino sketch.

Control Script

The main task of the control script named rotate is to rotate the pointer what is the current message to be displayed on the client reachable at <SERVER IP><SERVER PORT>/current. The pointer is a soft link that is moved forward every 30 seconds and wrapped around if the last file has been reached. The code looks like this:
#!/bin/bash
[[ "$PWD" == "/" ]] && exit 1
find -mmin +10 -name '*.txt' -exec rm {} \;
idx_file=".idx"
mapfile files < <(ls -1 *.txt)
[[ -e $idx_file ]] || echo -1 > $idx_file
idx=$(<$idx_file)
idx=$((idx+1))
[[ ${#files[@]} -le $idx ]] && idx=0
echo $idx>$idx_file
ln -sf ${files[$idx]} "current"
This script is executed by the following systemd timer and its companion service:
[Timer]
OnCalendar=*:*:0/30

[Install]
WantedBy=timers.target

----
[Unit]
Description=Rotate the file to be displayed

[Service]
Type=oneshot
ExecStart=<BASE_PATH>/ctrl-script/rotate
WorkingDirectory=<BASE_PATH>/data
User=bitclock

Data Scripts

Each data script gathers one piece of information from your server. These scripts are executed by the following systemd timer and its companion service:
[Timer]
OnCalendar=*:0/5

[Install]
WantedBy=timers.target
----
[Unit]
Description=Updates the data

[Service]
Type=oneshot
ExecStart=/usr/bin/run-parts .
WorkingDirectory=<BASE_PATH>/data-script
User=bitclock
In this example the data is refreshed every 5 minutes. If you have a lot of data scripts you could increase this number as it will take some time till the client has displayed each message once.
Important is that your scripts in the data-script directory are executable and properly named. The script name must not contain any extension. If you want to disable a script temporarily you can remove the execution permission and if you want to create a backup add an extension and then run-parts will not execute those scripts anymore.
All these data scripts source the following base code in order to reduce duplication:
set -eo pipefail
dest="../data/"$(basename $0)".txt"
trap 'echo "$label: n/a" > "$dest"; exit 0' ERR
If the execution of one of those scrips fails, then n/a will be displayed for that particular script. Each script writes the result into a file in the data directory named like the script adding the extension .txt to it.
In the next sections you can find some example of these data gathering scripts.

Block Height

This script queries the current block height from the bitcoin node:
#!/bin/bash
label="Block Height"
source ./base
echo "$label: $(bitcoin-cli getblockcount)" > $dest
Please make sure that the user executing the script - in this tutorial bitclock - has access to the bitcoin node by configuring a rpcuser in ~/.bitcoin/bitcoin.conf.

Bitcoin Price

For fetching the bitcoin price we can use coingecko.
#!/bin/bash
label="USD"
source ./base || exit 1
curl -x socks5h://localhost:9050 -s \
 "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd" |\
 jq -r '(.bitcoin.usd | tostring)' |\
 xargs -I{} printf "$label: %'d\n" {} > $dest

In order to this privately we are calling the API using tor on the default local port 9050 as a proxy. You can change the currency by changing the parameters accordingly.

Mining shares

The next script shows how we can fetch the current mining share from the datum gateway5:
#!/bin/bash
label="Shares"
source ./base || exit 1
curl -s http://localhost:<DATUM GATEWAY API PORT>/ |\
 pup "div.table-wrapper:nth-child(1) "\ "
 > div:nth-child(1) "\ "
 > table:nth-child(2) "\ "
 > tbody:nth-child(1) "\ "
 > tr:nth-child(1) "\ "
 > td:nth-child(2) text{}" | \
 cut -d' ' -f1 | \
 (echo -n "$label: " && cat) > $dest
In this example we actually have to parse the html page we receive from calling the datum gateway API. We do that with a CSS selector using pup6.

Forwarded Lightning Payments

The following script shows how you can fetch the count of the forwarded lightning payments of your local running lnd7 node for the day or day before if no payment have been forwarded yet:
#!/bin/bash
label="Lightning Tx"
source ./base || exit 1
lncli fwdinghistory --max_events=50000 -start_time -30d |\
        jq -r '.forwarding_events[] | [ (.timestamp | tonumber | strftime("%Y-%m-%d"))] | @tsv' | \
awk '{ count[$1]++ }\
     END   { for (d in count) print d":", count[d] } ' | \
sort |\
tail -1 |\
(echo -n "$label @" && cat) >$dest
Please make sure that the user executing the script has appropriate access to the lnd node.

Conclusion

Following this template you can add further scripts as for instance mempool size, feerate, number of peers, number of lightning channels etc.
With this setup you receive updates about the current state of the bitcoin network and your own infrastructure in a private and self-sovereign way.
Happy hacking :-)

Footnotes