miraiumのアイコン画像

デスク環境に緑を。マトリクスLEDを使ったGitHub Contributionインジケーター

miraium 2021年05月02日に作成  (2021年05月02日に更新)
デスク環境に緑を。マトリクスLEDを使ったGitHub Contributionインジケーター

GitHub Contributionインジケーター

はじめに

テレワークが始まり仕事とプライベートの線引きが難しくなってしまった。趣味プロジェクトでデスクに向かうのがなんとなく億劫になってしまった。そんな方は意外と多いのではないでしょうか?
私自身は、最近モチベーションを維持できず、仕事が終わった後はついダラダラ過ごしてしまい趣味のプログラミングプロジェクトの生産性が上がらず困っています。

ある日久々にGitHubにソースコードをアップし「前回からだいぶ間が空いてしまったなぁ...」なんて反省していたところ、 「このコントリビューションカレンダー(*)をいつもすぐ見える位置に置いておけば、もっと気が引き締まるのではないか?」 と、ふと思いつきました。

『毎日少しでも良いから進捗を出したい!自分の頑張りを可視化してモチベーションアップに繋げたい!』
そんな思いを実現し、生産性を向上させるべく、
今回GitHubのコントリビューションを可視化する装置を作成してみました。

自分の毎日の活動が自然と視界に入ってくるのは思っていた以上に効果的で、エディタを開く頻度は確実に上がったと思います。
点灯するLEDが毎日1つずつ増えていくのを見ていると「途切れさせたくない」という思いが強まり、何か作業したくなってきます。どことなくラジオ体操のスタンプを貯めていく感覚に近く、習慣化にも効果がありそうです。

obnizとマトリクスLEDを使うだけの簡単な装置なので、同じような思いを持っている人にもぜひお試しいただけたら幸いです。

(*) コントリビューションカレンダーとは、GitHubのプロフィールページで見られる緑色のアレです。GitHub上での活動(コントリビューション)の量が1日単位で緑色で可視化されていることから、GitHubで活動して緑のマスを増やすことを「草を生やす」なんてよく言いますね。毎日なにか活動して緑で一杯のカレンダーにしたいものです。
コントリビューションカレンダーの例

デモ動画・動作の仕組み

このアプリは、電源を入れてobnizのデバイススイッチを押すだけで動作します。
GitHubからコントリビューション情報を取得し、その情報をマトリクスLEDの点灯によって可視化してます。

ここに動画が表示されます

動画中にも触れていますが、動作の仕組みは以下の図のとおりです。
10分間の経過もしくはデバイススイッチの押下をトリガーとしてクラウドアプリが実行され、LEDの状態が更新されます。

LEDの状態更新完了後は、次回トリガーまでネットワークから切断されますが、obnizのIOの状態を維持する設定になっており、LEDは継続して点灯した状態となります。余計な点滅が無いため、デスクの上に置いてあっても集中力を乱すことは無いと思います。

動作の仕組み

部品

必須の部品

必須の部品は上図のとおり、obniz、マトリクスLEDモジュール、ピンヘッダ(L型)、ケーブルだけです。
なおケーブルについては、両端がメスになっている5ピンのケーブルがマトリクスLEDに付属していました。

部屋に置くインテリアらしさを出すため、100円ショップで手に入る木材を装置のベースとして使いました。

部品 個数 必須 購入先 備考
obniz Board 1Y 1個 obniz - 製品一覧 https://obniz.com/ja/products
マトリクスLEDモジュール 1個 Amazon - LEDドットマトリックス 8x8 ディスプレイモジュール Arduino対応 - 緑 https://www.amazon.co.jp/gp/product/B07XC11JBQ/ref=ppx_yo_dt_b_asin_title_o00_s00 MAX7219が使われた類似商品でも恐らく動作可。ケーブル付属。
L型ピンヘッダ 1個 秋月電子 - ピンヘッダ (L型) 1×6 (6P) https://akizukidenshi.com/catalog/g/gC-05336/
1.5cm角 木材 1本 100円ショップ (ダイソーなど) 装置ベース用。写真・デモ動画の装置には12.8cm分使用

設計図

以下の図のように、obniz Board 1YとマトリクスLEDを線で繋ぐだけOKです。

設計図 - obniz Board 1YとマトリクスLEDを接続するだけ

ピンNo. (obniz Board側) 接続先 (マトリクスLED側)
0 CLK
1 CS
2 DIN
3 GND
4 VCC

アプリインストール前の準備

今回obnizにインストールするアプリは、GitHub GraphQL APIを使ってコントリビューションカレンダーの情報を取得します。
GitHubとの通信において、以下2つが必要となるため、あらかじめメモ帳に控えておくなど準備しておくことをおすすめいたします。

1. カレンダー情報を取得したいアカウント名 (例: Miraium)
2. GitHub個人アクセストークン (取得方法の詳細はページ末尾の補足に掲載)

2. GitHub個人アクセストークンについて
個人アクセストークンは、GitHubへの認証でパスワードの代わりに使用する文字列です。具体的には『ghp_XXXXXXXXXX』のような長い文字列となっています。
他人に知られると悪用されてしまう可能性があるため、ソースコードに直接書いて公開したりしないよう、取り扱いには注意が必要です。
今回インストールするアプリは、obnizアプリの仕組みとして用意されている「インストール時設定」からアクセストークンを読み込む方式を採用しており、ソースコードに直接書かずに済むようになっていますのでご安心ください。

アプリ・ソースコード

アプリインストールについて

本アプリはobnizのウェブサイト上でアプリとして公開しています。
以下にアクセスして、簡単にインストールできるようになっています。

GitHub Contributionインジケーター
https://obniz.com/webapp/3301

まず、前章で準備した (1)アカウント名(2)GitHub個人アクセストークン を用意します。
続いて、インストール時設定のフォームに値をそれぞれ記入します。
最後に、インストールボタンを押すだけでアプリのインストール完了です。

アプリのインストールイメージ

(参考) ソースコード

設定の細かい変更や改造用にソースコードを掲載します。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"
    integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
    integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
    crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
    integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
    crossorigin="anonymous"></script>

  <script src="https://unpkg.com/obniz@3.14.0/obniz.js"></script>
</head>

<body>
  <div id="obniz-debug"></div>
  <br>
  <div class="text-center">
    <h1>Github Contribution Indicator</h1>
  </div>

  <script>

    // 設定値の定義
    const NUM_MATRIX = 4;
    const NUM_LED_ROW = 8;
    const NUM_LED_COL = 8;
    const LED_BRIGHTNESS = 5; // 0~15の整数値を入れる。暗くても十分視認できるため基本的に0でOK。
    const SORT_REVERSE_MATRIX_COL = true;
    const LED_REVERSE_ROW = false;
    const LED_REVERSE_COL = false;

    // 設定値に基づいて決まる定数
    const WIDTH = NUM_MATRIX * NUM_LED_COL;
    const HEIGHT = NUM_LED_ROW;

    // MAX7219のハードウェアタイプによっては、行が反転したり、マトリクス単位で逆順に表示されてしまうため、
    // LEDの表示反転・逆順表示対応版の関数
    function writeVram(revRow = false, revCol = false, revMatCol = false) {
      for (let line_num = 0; line_num < this.height; line_num++) {
        const addr = line_num + 1;
        var line_num_write = 0;
        if (revRow == true) {
          line_num_write = this.height - 1 - line_num;
        } else {
          line_num_write = line_num;
        }
        const line = this.vram[line_num_write];
        const data = [];
        for (let col = 0; col < line.length; col++) {
          var col_num_write = 0;
          if (revMatCol == true) {
            col_num_write = line.length - 1 - col;
          } else {
            col_num_write = col;
          }
          data.push(addr);

          let write_value;
          if (revCol == true) {
            write_value = eight_bits_reverse(line[col_num_write]);
          }else{
            write_value = line[col_num_write];
          }
          data.push(write_value);
        }
        this.write(data);
      }
    }

    // 8ビットの値をミラー反転させる関数
    // 1 (00000001) --> 128 (10000000)
    // 3 (00000011) --> 192 (11000000)
    // Ref.)
    // https://www.w3resource.com/javascript-exercises/javascript-basic-exercise-138.php
    function eight_bits_reverse(num) {
    	let result = 0;
      for (let i = 0; i < 8; i++) 
      {
		    result = result * 2 + (num % 2);
		    num = Math.floor(num / 2);
	    }
	    return result;
    }

    // Width列Height行のコントリビューションの有無を表す行列を生成する関数
    // コントリビューション有: 1
    // コントリビューション無: 0
    function convertCalendarToArray(contribCalendar, width = WIDTH, height = HEIGHT) {
      let array = Array.from(new Array(height), () => new Array(width).fill(0));
      for (let row in array) {
        for (let col in array[row]) {
          if (row < contribCalendar[col].contributionDays.length) {
            if (contribCalendar[col].contributionDays[row].contributionCount != 0) {
              array[row][col] = 1;
            } else {
              array[row][col] = 0;
            }
          }
        }
      }
      return array
    }

    // コントリビューションの有無を表す行列に基づいてLEDマトリクスを点灯情報をVramに書き込む関数
    function writeArrayToVram(array, matrix, num_matrix = NUM_MATRIX) {
      // 1つのLEDマトリクス1行分(LED8個分)の0or1の数値列を2進数とみなして数値に変換するための関数
      const reducer = (sum, currentValue) => sum * 2 + currentValue;

      // Matrix1の1行目、Matrix2の1行目 ,,, MatrixNの8行目の順に値をセットしていく
      // 値は2進数で1行分まとめてセットする
      for (let row in array) {
        line_all = array[row];
        for (let i = 0; i < num_matrix; i++) {
          index_start = i * 8;
          index_end = (i + 1) * 8;
          line = line_all.slice(index_start, index_end);
          line_val = line.reduce(reducer);
          matrix.vram[row][i] = line_val;
        }
      }
      matrix.writeVram(revRow = LED_REVERSE_ROW, revCol = LED_REVERSE_COL, revMatCol = SORT_REVERSE_MATRIX_COL);
    }

    function updateLedState(github_access_token, account_name, matrix) {
      fetch('https://api.github.com/graphql', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'Authorization': `Bearer ${github_access_token}`,
        },
        body: JSON.stringify({
          query: `
            {
                user(login: "${account_name}") {
                  id
                  bio
                  email
                  contributionsCollection {
                    contributionCalendar {
                      totalContributions
                      weeks {
                        contributionDays {
                          contributionCount
                          date
                          weekday
                        }
                      }
                    }
                  }
                }
              }          
            `
        })
      }).then(function (res) {
        return res.json();
      }).then(function (data) {
        let bio = data.data.user.bio;
        // 最新のWIDTH週分取得
        let contribCal = data.data.user.contributionsCollection.contributionCalendar.weeks.slice(-WIDTH);
        let array = convertCalendarToArray(contribCal);
        writeArrayToVram(array, matrix);
      }
      )
    }

    /* This will be over written on obniz.io webapp page */
    var obniz = new Obniz("OBNIZ_ID_HERE");
    let install_configration = Obniz.App.configs();
    let access_token = install_configration['github_graphql_access_token'];
    let account_name = install_configration['github_account_name'];
    obniz.onconnect = async function () {
      obniz.reset();

      const matrix = obniz.wired("MatrixLED_MAX7219", { clk: 0, cs: 1, din: 2, gnd: 3, vcc: 4 });
      matrix.writeVram = writeVram;
      matrix.init(WIDTH, HEIGHT);
      matrix.brightness(LED_BRIGHTNESS);
      // まずtest()を呼び出すことで、最後の点灯状態を復元する。
      matrix.test();  

      // ディスプレイにはアカウント名を表示
      obniz.display.print("[Account]");
      obniz.display.print(account_name);
      // LEDの状態を更新
      updateLedState(access_token, account_name, matrix);

      // ネットワークとの接続が切れた後もLEDの状態を維持するように設定
      obniz.resetOnDisconnect(false);
      
      // 5秒待機して明示的に接続を切る
      await obniz.wait(5000);
      obniz.close();
    }

  </script>
</body>

</html>

動作確認・仕上げ

アプリのインストール・LEDの接続が完了したら、電源を入れてobnizのデバイススイッチを押すだけです。
GitHubのコントリビューションカレンダーとLEDの点灯状態が一致していることを確認できればOKです。

動作確認

マトリクスLEDモジュールのハードウェア仕様によっては、8x8を1単位として点灯位置が左右反転してしまう現象などが生じるようです。
詳細は補足の章に記載しますが、ソースコード中の反転フラグを調整してみると解消できるかもしれません。

最後に、インテリア的にディスプレイの上に置けるよう、木材をベースとして各種部品を一体化しました。
両面テープで簡単に固定しているだけですが、うまくコンパクトに収まりました。裏から見た写真はこんな感じです。
(obniz本体のピンソケットが両面テープを貼るのに丁度良い位置・面積でした。)

仕上げ - 部品を木材へ一体化

おわりに

obnizとマトリクスLEDモジュールを組み合わせただけの簡単な装置ですが、作ってみると想像以上にLEDの発色が良く、コンパクトなのでインテリア的に置けるのがとても気に入っています。
今後、もう少しメンテナンスして見た目の改善を図りたいと思っています。細かなはんだ付けが必要なので、いつか時間のあるときにじっくり取り組みたいと思います。

今後の改善予定

このマトリクスLEDモジュールは横方向が8x4=32列しか無いため、1年分(53週分)を表示しようとすると後21列必要です。
マトリクスLEDの点灯にはMAX7219というチップが使われていて同じモジュールを買えば数珠繋ぎで増やせるようです。追加購入してはんだ付けして配線しようと思います。

また、購入したマトリクスLEDモジュールは、あらかじめ横側にピンが出ているため接続が簡単でありがたいのですが、正面から見ると配線が見えてしまって少しかっこ悪いです。
はじめから付いていたピンを外して、裏側からピンを出すように修正したいと思います。これにもはんだ付け作業が必要になりそうです。

正面から見た図

感想・まとめ

obnizはブラウザ上で手軽に扱えてライブラリも充実していて、試行錯誤がしやすかったです。
この装置を作ってみようと思い立ってから約1週間で基本機能が実現できました!その後も頭の中で思い描いていたモノがどんどん形になっていくのが楽しくて、ペースを維持して開発を続けられました。

他のIoT向けボードでも似たような装置を作ることはきっと出来ると思うのですが、導入までのハードルは高くなりがちな印象です。今回の制作にobnizを使ったのは我ながらベストな選択だったのではないかと思っています。

今回作ったGitHub Contributionインジケーターを普段の生活で使いながら、今後もいろいろなプロジェクトを前向きに、生産性高く進めていきたいと思います!ここまで読んでいただきありがとうございました!


補足

(補足1) GitHub個人アクセストークンの取得方法

GitHubの個人アクセストークンの取得方法について、スクリーンショットを交えながら簡単にまとめます。
大まかな手順は、以下のとおりです。

  1. ログインして右上メニューからSettingsへ
  2. Account settingsメニューのDeveloper settingsへ
  3. 左メニューのPersonal access tokensを押す
  4. Generate new tokenボタンをクリック
  5. Noteには自分がわかりやすいようにメモを取り、Select scopesでGraphQL APIの使用に必要な項目をチェック
  6. ページ下のGenerate tokenボタンで、個人アクセストークンを作成
  7. 表示された個人アクセストークンをメモに控えて完了 (表示されるのは1度だけなので注意)

以下、スクリーンショットで説明を進めていきます。

[1. ログインして右上メニューからSettingsへ]
まずSettingsページヘ移動します。

1. ログインして右上メニューからSettingsへ

[2. Account settingsメニューのDeveloper settingsへ]
続いてDeveloper settingsページヘ移動します。

2. Account settingsメニューのDeveloper settingsへ

[3. 左メニューのPersonal access tokensを押す]
次にPersonal access tokensを押して、個人アクセストークンの作成に取り掛かります。

3. 左メニューのPersonal access tokensを押す

[4. Generate new tokenボタンをクリック]
Generate new tokenボタンをクリックすると、新規に個人アクセストークンを作成できます。

4. Generate new tokenボタンをクリック

[5. Noteには自分がわかりやすいようにメモを取り、Select scopesでGraphQL APIの使用に必要な項目をチェック]
[6. ページ下のGenerate tokenボタンで、個人アクセストークンを作成]
個人アクセストークンの作成では、何用のアクセストークンなのか後々判別できるよう、Noteにメモを残しておきます。
そして、GraphQL APIでの通信リクエストに必要な項目をチェックしていきます。
最後に、Generate tokenボタンで、個人アクセストークンが作成されます。

なお、チェックする項目については、以下のGitHub公式ページに記載があります。記載のとおりにチェックを入れます。
GraphQLでの呼び出しの作成 - GitHub Docs

user
public_repo
repo
repo_deployment
repo:status
read:repo_hook
read:org
read:public_key
read:gpg_key

5. Noteには自分がわかりやすいようにメモを取り、Select scopesでGraphQL APIの使用に必要な項目をチェック

6. ページ下のGenerate tokenボタンで、個人アクセストークンを作成

[7. 表示された個人アクセストークンをメモに控えて完了 (表示されるのは1度だけなので注意)]
個人アクセストークンの作成が完了すると、自動的にページが遷移し、長い文字列が表示されます。これが個人アクセストークンです。
一度しか表示されないため、必ずメモに控えるようにします。クリップボードボタンを押してコピーするのが安心です。

7. 表示された個人アクセストークンをメモに控えて完了 (表示されるのは1度だけなので注意)

これで個人アクセストークンの取得は完了です。

(補足2) LEDの点灯位置がおかしい場合

マトリクスLEDについては、形が類似した商品が多く出回っており入手自体は容易なのですが、
ハードウェア仕様が微妙に異なることがあるようで、同じように接続しても表示が反転するなどの現象がたまに生じます。

ソースコードの設定値を変更するだけで様々なパターンに対応できるよう関数を準備しておきましたので、表示がおかしい場合には設定値の変更をお試しください。

この章では、以下の図のとおり、正しい表示が「ABCD」だとして、表示がおかしい代表的なパターンを取り上げて、設定値の変更内容を説明します。全ては網羅できていませんがご参考になれば幸いです。

マトリクスLED - 正しい表示内容

以下、それぞれのパターンに応じて、設定値の変更方法を記載します。

パターン1: マトリクスLED単位で順番が逆 (DCBAになってしまう)

// 設定値の定義
    // ~~~
    const SORT_REVERSE_MATRIX_COL = false; // true --> falseに変更した
    const LED_REVERSE_ROW = false;
    const LED_REVERSE_COL = false;

パターン2: 1つのマトリクスLEDの中で点灯内容が反転

// 設定値の定義
    // ~~~
    const SORT_REVERSE_MATRIX_COL = true;
    const LED_REVERSE_ROW = false;
    const LED_REVERSE_COL = true; // false --> trueに変更した

パターン3: マトリクスLEDの順番が逆かつ点灯内容も反転 (パターン1とパターン2の複合)

// 設定値の定義
    // ~~~
    const SORT_REVERSE_MATRIX_COL = false; // true --> falseに変更した
    const LED_REVERSE_ROW = false;
    const LED_REVERSE_COL = true; // false --> trueに変更した

パターン4: 上下方向に点灯内容が反転

// 設定値の定義
    // ~~~
    const SORT_REVERSE_MATRIX_COL = true;
    const LED_REVERSE_ROW = true; // false --> trueに変更した
    const LED_REVERSE_COL = false;

表示がおかしくなる問題について、すべてのパターンの説明はしきれておりませんが、これらの複合でおおよそ対応可能と思われます。

1
miraiumのアイコン画像
https://twitter.com/miraium
miraium さんが 前の日曜日の15:07 に 編集 をしました。 (メッセージ: 初版)
miraium さんが 前の日曜日の15:13 に 編集 をしました。 (メッセージ: 動画埋め込み・見出しのインデックス調整)
miraium さんが 前の日曜日の15:21 に 編集 をしました。 (メッセージ: obniz boardの購入先URL追加)
ログインしてコメントを投稿する