The Onist

The Onist


Tags


Build A Raspberry Pi Bitcoin Hub - Part 3

The final part of the 3 part tutorial explaining how to build a Bitcoin price tracking Electron application intended for use on the Raspberry Pi.

In my previous posts, we have covered setting up our Pi for development with NodeJs (part 1) and overviewed Electron and the serverside Express code in (part 2).

This post will cover the frontend Angular code in detail and then end with a quick explanation on how to autostart our new app when we boot the Pi. Without further ado, let's dive into the code!

Bootstrapping Angular

The entry point into the single page application is the index.html. This file loads all vendor libraries and CSS files in the head tag and all application code at the bottom of the body.

<!DOCTYPE html>
<html>
  <head>
    <title>Bitcoin Stats</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic">
    <link rel="stylesheet" href="bower_modules/angular-material/angular-material.css">
    <link rel="stylesheet" href="bower_modules/angular-material-data-table/dist/md-data-table.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="css/main.css">
    <!-- client side libraries -->
    <script type="text/javascript" src="bower_modules/lodash/lodash.js"></script>
    <script type="text/javascript" src="bower_modules/moment/min/moment.min.js"></script>
    <script type="text/javascript" src="bower_modules/socket.io-client/dist/socket.io.slim.js"></script>
    <script type="text/javascript" src="bower_modules/amcharts3/amcharts/amcharts.js"></script>
    <script type="text/javascript" src="bower_modules/amcharts3/amcharts/serial.js"></script>
    <script type="text/javascript" src="bower_modules/angular/angular.js"></script>
    <script type="text/javascript" src="bower_modules/angular-animate/angular-animate.js"></script>
    <script type="text/javascript" src="bower_modules/angular-aria/angular-aria.js"></script>
    <script type="text/javascript" src="bower_modules/angular-material/angular-material.js"></script>
    <script type="text/javascript" src="bower_modules/angular-ui-router/release/angular-ui-router.js"></script>
    <script type="text/javascript" src="bower_modules/angular-material-data-table/dist/md-data-table.js"></script>
  </head>
  <!-- init angular app -->
  <body ng-app="app" layout="row">
    
    <!-- sidenav -->
    <md-sidenav
      ng-include
      src="'js/layout/sidenav.html'"
      ng-controller="sidenavCtrl"
      layout="column"
      class="md-sidenav-left md-whiteframe-z2"
      md-is-locked-open="true">
    </md-sidenav>

    <div layout="row" class="relative" layout-fill role="main">
      <md-content layout="column" flex md-scroll-y>
        <md-card flex>
          <md-card-content flex ui-view>
            <!-- page content loads here -->
          </md-card-content>
        </md-card>
      </md-content>
    </div>
    
    <!-- app source -->
    <script src="js/app.js"></script>
    <script src="js/layout/sidenav.controller.js"></script>
    <script src="js/markets/markets.controller.js"></script>
  </body>
</html>

We then configure angular module dependencies, register application routes via Angular UI Router and configure our theme in app.js.

(function(){
  'use strict';

  angular
    .module('app', ['ngMaterial','ngAnimate','ui.router','md.data.table','app.markets','app.blocks','app.network'])
    .config(['$stateProvider', '$urlRouterProvider', '$mdThemingProvider',
      function config($stateProvider, $urlRouterProvider, $mdThemingProvider) {
        // configure app states
        $stateProvider.state({
          name: 'markets',
          url: '/markets',
          controller: 'marketsCtrl',
          templateUrl: 'js/markets/markets.html'
        });

        // when there is an empty route, redirect to /markets
        $urlRouterProvider.when('', '/markets');

        // configure theme
        $mdThemingProvider.theme('default')
          .primaryPalette('teal')
          .accentPalette('red');
      }
    ]);
})();

Building the Sidenav Component

The sidenav of our application is built with a simple template and controller file. Lines 30-37 of the index.html file above initiate this component by including the template via an ng-include and the controller via ng-controller.

The controller is responsible for creating the sidenav links via the $scope.menu array which will be used later in our template. It is also responsible for calling to our server at /exchangePrices to fetch current prices from the different exchanges and setting that on $scope. In the callback of the $http.get method we use Lodash methods to filter and sort our data for the exchanges we choose to list. In this case the exchanges are Gemini, GDAX, Bitstamp and Bitfinex. We also set an interval via $interval to continuously fetch the latest prices every 20 minutes.

angular.module('app').controller('sidenavCtrl', function ($scope, $http, $interval) {
  $scope.menu = [{
    state: 'markets',
    title: 'Markets',
    icon: 'timeline'
  }];

  $scope.getExchangePrices = function() {
    $http.get('/exchangePrices')
      .then(function(response) {
        // pull only the exchanges we care about and sort by name
        $scope.exchanges = _.sortBy(_.filter(response.data, function(e) {
          return e.name === 'bitstamp' || e.name === 'bitfinex' || e.name === 'gdax' || e.name === 'gemini';
        }), function(exchanges) {
          return exchanges.name
        });

        // format last updated timestamp
        $scope.lastUpdated = window.moment().format('h:mm A');
      });
  };

  // update prices every 20 minutes
  $interval(function() {
    $scope.getExchangePrices();
  }, 200000);

  // immediately fetch exchange prices
  $scope.getExchangePrices();
});

The template for the sidenav uses a few angular material components (md-toolbar and md-table-container) to render correctly. I added a refresh button to allow the user to manually refresh prices at will. I also make use of ng-class to color prices green or red to indicate prices changes either positive or negative after the latest updated.

<md-toolbar class="md-hue-2" layout-align="center center" layout-padding>
  Bitcoin Statistics
</md-toolbar>
<ul class="sidenav">
  <li ng-repeat="item in menu">
    <md-button ui-sref="{{item.state}}" ui-sref-active="activeNav">
      <md-icon>{{item.icon}}</md-icon>
      <span>{{item.title}}</span>
    </md-button>
  </li>
</ul>
<span flex></span>
<md-table-container>
  <table md-table id="sidebarTable">
    <thead md-head>
      <tr md-row>
        <th md-column>Exchange</th>
        <th md-column md-numeric>Price</th>
        <th md-column md-numeric>Volume</th>
      </tr>
    </thead>
    <tbody md-body>
      <tr md-row ng-repeat="stats in exchanges">
        <td md-cell>{{stats.display_name}}</td>
        <td ng-class="{
          'priceUp': stats.symbols.BTCUSD.last < stats.symbols.BTCUSD.ask,
          'priceDown': stats.symbols.BTCUSD.last > stats.symbols.BTCUSD.ask }" md-cell>
          <span ng-if="stats.symbols">${{stats.symbols.BTCUSD.last}}</span>
          <span ng-if="!stats.symbols">N/A</span>
        </td>
        <td md-cell>
          <span ng-if="stats.symbols">{{stats.symbols.BTCUSD.volume}}</span>
          <span ng-if="!stats.symbols">N/A</span>
      </tr>
    </tbody>
  </table>
</md-table-container>
<div id="lastUpdated" layout="row" layout-align="space-around center">
  <div>
    Last Updated: {{lastUpdated}}
  </div>
  <md-button class="md-icon-button" aria-label="Refresh" ng-click="getBtcPrice()">
    <md-icon>refresh</md-icon>
  </md-button>
</div>

Building the Markets Graph

The Markets page shows the user an interactive graph of the Bitcoin price over a given time period. The default timespan is 30 days but is changeable via a dropdown menu. AmCharts is used to create and draw the graph.

The controller for this component is actually quite simple with the bulk of the code used to configure the AmChart. As you can see in the code below, I'm using a $scope.$watch on the timePeriod variable to listen for user changes to the timespan. When a change is detected, an HTTP request is made to /priceChart?timespan={} to fetch data for the new timespan. Note that this snippet of code will also fire when the controller is initialized.

I will not cover the AmCharts setup in this tutorial but if you're interested in learning more and configuring the graph further, there is a really slick online editor available to you.

angular.module('app.markets', []).controller('marketsCtrl', function ($scope, $http) {
  $scope.isLoading = true;
  $scope.timePeriod = '30d';

  // watch for user changes to timePeriod dropdown
  $scope.$watch('timePeriod', function (newVal) {
    $scope.isLoading = true;

    $http.get('/priceChart?timespan=' + newVal)
      .then(function(resp) {
        // convert unix timestamps to JS date objects
        _.each(resp.data, function(point) {
          point.x = AmCharts.formatDate(new Date(parseInt(point.x) * 1000), "M/D/YY");
        });

        _buildChart(resp.data);
        $scope.isLoading = false;
      });
  });

  function _buildChart(chartData) {
    // configuration for AmChart
    AmCharts.makeChart("priceChart", {
      type: "serial",
      categoryField: "x", // date
      dataDateFormat: "MM/DD/YY",
      startDuration: 1,
      handDrawScatter: 1,
      precision: 2,
      processCount: 1005,
      theme: "default",
      categoryAxis: { gridPosition: "start" },
      chartCursor: {
        limitToGraph: "priceChart",
        enabled: true
      },
      chartScrollbar: { enabled: true },
      graphs: [{
        balloonText: "$[[value]]",
        valueField: "y" // price
      }],
      guides: [],
      valueAxes: [{
        title: "Price",
        unit: '$',
        unitPosition: 'left'
      }],
      balloon: {},
      titles: [{
        size: 18,
        color: 'rgb(0,105,92)',
        text: "Average Market Price (USD)"
      }],
      dataProvider: chartData
    });
  }
});

The template is very simple with really the only interesting thing of note being the dropdown menu. This menu is created using the md-select and md-option directives. You'll also notice that I'm using the ng-show and ng-hide directives to the show/hide the loading spinner and the graph based on whether data has been returned by the server. The graph will be drawn as an SVG inside <div id="priceChart"> when the data is loaded.

<div ng-show="isLoading" flex layout="row" layout-align="space-around center">
  <md-progress-circular md-mode="indeterminate"></md-progress-circular>
</div>
<div ng-hide="isLoading" layout="row" layout-align="end" id="timePeriodSelect">
  <md-select ng-model="timePeriod" placeholder="Time Period" class="md-no-underline">
    <md-option value="30d">1 Month</md-option>
    <md-option value="90d">3 Month</md-option>
    <md-option value="180d">6 Month</md-option>
    <md-option value="1year">1 Year</md-option>
    <md-option value="4year">All</md-option>
  </md-select>
</div>
<div ng-hide="isLoading" id="priceChart"></div>

Running on the Raspberry Pi

When I initially started this project, I was hoping to package this app with an installer targeted for the Raspberry Pi. However, it is currently not possibe, or at least easy, to create an electron installer targeted for Linux ARM. See this electron-builder issue for more information. With that being said, if you'd like to create installers for OSX or Windows here is a guide on how to do so.

So instead of creating an installer for this application, I will quickly guide you through the process of setting up the app directly on the Pi and then configuring the app to autostart. Simply follow the instructions below!

  1. Clone this repo to a folder of your choice onto the Pi.
  2. From the root of the cloned repo, run npm install && bower install to pull the dependencies of the application.
  3. Modify line 5 of the included btc-autostart.sh file to cd into the root directory of your cloned app.
  4. As root, run the following commands to make the btc-autostart.sh file executable by Linux:
sudo chown pi:pi /path/to/btc-autostart.sh 
sudo chmod +x /path/to/btc-autostart.sh
  1. Edit your Pi's LXDE autostart.sh file to run this applications btc-autostart.sh file via the command below:
sudo nano /home/pi/.config/lxsession/LXDE-pi/autostart.sh
  1. At the end of this same file, add a line similar to below that, that points to this applications btc-autostart.sh file.
/path/to/btx-autostart.sh
  1. Save and exit the file.
  2. Restart your Pi and the app should now automatically start.

Conclusion

Well there you have it, a working Raspberry Pi Bitcoin Statistics application!  Hopefully you had as much fun learning and building this application as I did.  If you wish to continue developing this application and even contributing back to the repo, feel free!

Share Post
View Comments