ODROID-C2にNode.jsをインストールし、GPIOを使えるようにする


# 概要
僕はPythonがピーマンより苦手なので、ODROID-C2にNode.jsをインストールして、JavaScriptで動かせる環境を構築します。

RaspberryPiと異なる点は、64bit版OSなので、ARMv8(arm64)バイナリが実行可能な所です。


# 実行環境
- Board: [ODROID-C2](http://www.hardkernel.com/main/products/prdt_info.php?g_code=G145457216438)
- OS: [Ubuntu Mate 16.04LTS AArch64](https://wiki.odroid.com/odroid-c2/os_images/ubuntu/v2.4)
- Node.js: [10.1.0 ARMv8](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V10.md#10.1.0)


# 1. Node.jsのインストール
## 1-1. いつもの
```shell
sudo apt update
sudo apt upgrade
sudo apt autoremove
```
まずはここから。
最近は  ``apt-get`` より ``apt`` が流行りのようですね。


## 1-2. PATH通し(初回のみ)
```shell:~/.profile
# Node.js
export nodejs=
export nodemodules=
export PATH=${nodejs}:${nodemodules}:${PATH}
```
後述のスクリプトとの兼ね合いで、先にPATHを追記しておきます。


## 1-3. ダウンロード & インストール
```shell:nodejs-install.sh
if [ $# -ne 1 ]; then
    echo "Version number is required as argument."
    echo "Example: sudo ./nodejs-install.sh 10.0.0"
    exit 1
fi

version=${1}
filename=node-v${version}-linux-arm64
directory=/usr/local/lib/nodejs

mkdir -p ${directory}
cd ${directory}

if [ -e ${filename} ]; then
    echo "Version ${version} is already installed."
    exit 1
fi

wget https://nodejs.org/dist/v${version}/${filename}.tar.xz
tar -Jxvf ./${filename}.tar.xz
rm -rf ./${filename}.tar.xz

sed -ie "s|nodejs=.*$|nodejs="${directory}"/"${filename}"/bin|g" ~/.profile
sed -ie "s|nodemodules=.*$|nodemodules="${directory}"/"${filename}"/lib/node_modules|g" ~/.profile

source ~/.profile
```
ダウンロード & インストール & PATH変更
全部お任せスクリプトを書いてみました。

```shell
sudo ./nodejs-install.sh 10.1.0
```
インストールしたいバージョンを引数で渡すだけ。

## 1-4. 動作確認
```shell
node -v
npm -v
```
再起動後、適当にコマンドを入力して応答があったら、インストールは無事完了です。


# 2. GPIOのセットアップ
## 2-1. アクセス権の設定

```shell
sudo touch /etc/udev/rules.d/90-odroid-sysfs.rules
```
```shell:90-odroid-sysfs.rules
KERNEL=="gpiochip*"
SUBSYSTEM=="gpio"
ACTION=="add"
PROGRAM="/bin/sh -c 'chown root:gpio /sys/class/gpio/export /sys/class/gpio/unexport ; chmod 220 /sys/class/gpio/export /sys/class/gpio/unexport'"

KERNEL=="gpio*"
SUBSYSTEM=="gpio"
ACTION=="add"
PROGRAM="/bin/sh -c 'chown root:gpio /sys%p/active_low /sys%p/direction /sys%p/edge /sys%p/value ; chmod 660 /sys%p/active_low /sys%p/direction /sys%p/edge /sys%p/value'"
```
空のルールファイルを作成し、中にルールを記述します。

```shell
sudo addgroup gpio
sudo usermod -a -G gpio odroid
```
後は`gpio`と言うグループを追加して、先ほどのルールを適用させます。


## 2-2. PWM有効化
```shell
# 1系統(SPI使用可)
sudo modprobe pwm-meson
sudo modprobe pwm-ctrl

# 2系統(SPI使用不可)
# sudo modprobe pwm-meson npwm=2
# sudo modprobe pwm-ctrl
```
PWMは再起動毎に無効化されてしまうので、毎回このコマンドが必要になります。
面倒であれば、rcスクリプトに登録しておくと良いかも知れません。

ODROID-C2は2系統のPWMを持っていますが、片方はSPIと排他となっています。


## 2-3. Node.js向けGPIOラッパー
ODROID-C2用のモジュールが無かったので、作ってみました。
気が向いたら`npm`にアップロードするかも?

<details><summary>JavaScript</summary><div>

```javascript:c2io.js
"use strict";

const execSync = require("child_process").execSync;

const gpioDirectory = "/sys/class/gpio";
const pwmDirectory = "/sys/devices/platform/pwm-ctrl";
const adcDirectory = "/sys/class/saradc";

const pinMap = {
  7: 249,
  11: 247,
  12: 238,
  13: 239,
  15: 237,
  16: 236,
  18: 233,
  19: 235,
  21: 232,
  22: 231,
  23: 230,
  24: 229,
  26: 225,
  29: 228,
  31: 219,
  32: 224,
  33: 234,
  35: 214,
  36: 218
};

function xor(a, b){
  return (a || b) && !(a && b);
}

exports.C2GPIO = class C2GPIO{
  constructor(){}

  open(pin, direction){
    if(!pinMap[pin] || !xor(direction != "in", direction != "out")){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo " + pinMap[pin] + " > " + gpioDirectory + "/export");
    execSync("echo " + direction + " > " + gpioDirectory + "/gpio" + pinMap[pin] + "/direction");
    this.write(pin, 0);
    return true;
  }

  close(pin){
    if(!pinMap[pin]){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo " + pinMap[pin] + " > " + gpioDirectory + "/unexport");
    return true;
  }

  read(pin){
    if(!pinMap[pin]){
      console.log("Argument Invalid");
      return false;
    }
    return Number(execSync("cat " + gpioDirectory + "/gpio" + pinMap[pin] + "/value").toString());
  }

  write(pin, value){
    if(!pinMap[pin] || !xor(value != 1, value != 0)){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo " + value + " > " + gpioDirectory + "/gpio" + pinMap[pin] + "/value");
    return true;
  }
}

exports.C2PWM = class C2PWM{
  constructor(){}

  start(ch){
    if(!xor(ch != 0, ch != 1)){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo 1 > " + pwmDirectory + "/enable" + ch);
    return true;
  }

  stop(ch){
    if(!xor(ch != 0, ch != 1)){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo 0 > " + pwmDirectory + "/enable" + ch);
    return true;
  }

  setDuty(ch, ratio){
    if(!xor(ch != 0, ch != 1) || (ratio < 0 || 1024 < ratio)){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo " + (1023 / 100 * ratio) + " > " + pwmDirectory + "/duty" + ch);
    return true;
  }

  setFreq(ch, frequency){
    if(!xor(ch != 0, ch != 1) || (frequency < 0 || 1000000 < frequency)){
      console.log("Argument Invalid");
      return false;
    }
    execSync("echo " + frequency + " > " + pwmDirectory + "/freq" + ch);
    return true;
  }
}

exports.C2ADC = class C2ADC{
  constructor(){}

  read(ch){
    if(!xor(ch != 0, ch != 1)){
      console.log("Argument Invalid");
      return false;
    }
    return Number(execSync("cat " + adcDirectory + "/ch" + ch).toString());
  }
}

```
</div></details>

## 2-4. 動作確認
```javascript:gpio-test.js
"use strict";

const c2io = require("./c2io");
const gpio = new c2io.C2GPIO();

gpio.open(7, "out");
gpio.write(7, 1);
```
```shell
node gpio-test.js
```
上記の自作モジュールをインポートしています。
7番ピンが`HIGH`になるはずなので、電圧計やLEDを繋いで確認します。

ここまで無事に動けば、目標達成です。


# まとめ
公式配布のバイナリを使用する事で、ビルドの手間が省け、思ってたよりも簡単に構築出来ました。
もっと躓くかと思っていましたが、全然そんな事は無かったです。
これで、Expressとかに組み合わせて、イケてるIoTを開発する準備が整いました。

お疲れ様でした。


# 自作GPIOモジュール補足
sysfs制御なので、スイッチング/サンプリングは1~2kHz程度と低速です。

高速スイッチングが必要な場合は、PWMを推奨します。
ODROID-C2のPWM仕様は下記となっています。

- 周波数: 最大1MHz
- デューティ精度: 1024段階(10bit)

(`/dev/gpiomem`直叩きだと1MHz以上も余裕らしい...)


# 参考/引用(Special Thanks!)
- [Installation - nodejs](https://github.com/nodejs/help/wiki/Installation)
- [ODROID-C2 - ODROID Wiki](https://wiki.odroid.com/odroid-c2/odroid-c2)
- [pi-gpio - GitHub](https://github.com/rakeshpai/pi-gpio)