BeagleBone BlackのWebサーバでシリアル通信

BeagleBone BlackをWebサーバにしてシリアルポートを持つデバイスをWebから制御する。
例えば[イメージニクス](http://www.imagenics.co.jp/)のマトリクススイッチャとか。
今回、以前セットアップしたBBBのバックアップ機を用意することにしたので、その記録。

### 環境
- BeagleBone Black Rev. C
- Debian 8.5
- Apache 2.4.10
- HTML + JavaScript + python (CGI)

## BeagleBone Blackのセットアップ
基板上のeMMCにDebianがインストール済ですが、余計なものが入ってたりするのでmicroSDにインストール。
Windows上で[BeagleBoardDebian - eLinux.org](http://elinux.org/BeagleBoardDebian)のraw microSD imgのBeagleBone Black用イメージをダウンロードし、Win32 Disk Imagerで書き込み。
余計なものが入ってなく、容量の拡張方法も書いてあるので良いです。

## シリアル通信
Web系の準備の前にシリアル通信の確認をしておきます。
BBBにシリアルポートは無いのでUSB-RS232Cの変換ケーブルを使います。接続すると/dev/ttyUSB0とかデバイスが追加される。

```
# cat /dev/ttyUSB0                 # 受信
# echo 'hoge' > /dev/ttyUSB0       # 送信
```

Linux同士でクロスケーブル繋いだりして確認。シリアルポートさえ準備できれば特に問題ないでしょう。
何かうまくいかないなあと思った時はとりあえずリブートしてます。

### Pythonでシリアル通信
pyserialというライブラリがあるのでこれを使わせていただく。
apt update&upgradeしないと多分python-pipが見つからないと思います。

```
apt install python-pip
pip install pyserial
```

インストールしたらコマンドラインのpythonで確認すると良いと思います。

```
>>> import serial
>>> s = serial.Serial('/dev/ttyUSB0')
>>> s.readline()
>>> s.write('hoge')
>>> s.close()
```

## Webサーバ
インストール済のApacheを使います。
BeagleBoard.orgとかから持ってきたイメージだとWebサーバ周りでbonescript-autorunだとかのプログラムが走っていて止めたりポート変えたりしないといけなくて面倒です。

### CGIを使えるようにする
いくつか設定が必要です。

*/etc/apache2/conf-available/serve-cgi-bin.conf*
/usr/lib/cgi-bin/をcgiを置くディレクトリに変更する。

```/etc/apache2/conf-available/serve-cgi-bin.conf
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
```

*/etc/apache2/sites-available/000-default.conf*
以下の行のコメント解除。

```/etc/apache2/sites-available/000-default.conf
# Include conf-available/serve-cgi-bin.conf
```

あと、
`apache2ctl -M | grep cgi`
を実行してcgi_moduleが表示されない場合はcgiモジュールを読み込ませないといけないらしい。

```
cd /etc/apache2/mods-enabled
ln -s ../mods-available/cgi.load .
```
[こちらのページ](https://tech.nosuz.jp/2015/11/load-cgi-module/)に大変助けられました。

### その他の設定
ApacheからLinuxのデバイスにアクセスするには権限を付与する必要があります。

```
# ll /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0 Jul 12 06:14 /dev/ttyUSB0
```
(他は知りませんが)BBBでのApacheのユーザ名はwww-dataなのでdialoutグループに追加。
`gpasswd -a www-data dialout`
リブートして有効化します。
私は閉じたネットワークで使用しているので気にしませんが他にデバイス等が色々ある場合はセキュリティ的に問題になる場合があるのかもしれません。

### Webからシリアル通信
python CGIを作成し、JavaScriptから読み込む。
以下は制御コマンドを1つ送信してその応答を受け取る例。JavaScriptで指定した引数によって制御コマンドを決めるのが普通かと思います。

```python:serial.cgi
#!/usr/bin/python
# -*- coding: utf-8 -*-

print "Content-type: text/plain\n\n"

import cgi
import serial
import io
import threading

class ReadLine_Thread(threading.Thread):

	def __init__(self, sp):
		super(ReadLine_Thread, self).__init__()
		self.ser = sp
		
	def _readline(self):
		self.ser_io = io.TextIOWrapper(io.BufferedRWPair(self.ser, self.ser, 1),
										newline = '\r',
										line_buffering = True)
		try:
			rcv = self.ser_io.readline()
		finally:
			self.ser.close()

		return rcv

def send():
	s = serial.Serial('/dev/ttyUSB0', timeout=0.1)
	# 応答受信用スレッド
	th_rl = ReadLine_Thread(s)
	th_rl.start()

	s.write('ここに制御コマンド(f['hoge'].valueなどを使う)' + '\r')

	r = th_rl._readline()
	
	s.close()

	return r

##########
# main
##########

f = cgi.FieldStorage()

print send()
```

ボーレートにもよるかもしれないけど予め受信スレッドを動かしておかないと応答を受け取れないと思います。
_readlineの最初の行は改行コードを指定しています。相手のデバイスは取説が不親切な物があるのでどれにすれば応答を受け取れるか変えて試してました。

…これはかなり省略して書いてますが、実際には操作コマンドを送った後に状態取得コマンドを送信して応答をもらいブラウザに反映させる…とか色々書かないといけなくなります。デバイス側はコマンドを送る度にいらないパラメータでも返してきたりすることがあるのでreadlineを余分に書くとかが必要になってきます。

```js:serial.js
function exec_cgi(hoge) {
	return $.ajax({
		type     : 'POST',
		url      : '/cgi-bin/serial.cgi',
		data     : {
			'hoge' : hoge
		},
		dataType : 'text'
	});
}
```

JavaScriptはこんな感じのメソッドを作ってCGIを呼んでいます。
あとは

```
exec_cgi('hoge').done(function(rcv) {
	...
})
```
という感じで応答を処理していけばOK。

## 参考
[Pythonでシリアル通信](http://qiita.com/kosystem/items/0023cfee941fdf099087)
[Linuxにおけるシリアルポートのトラブルシューティング](http://qiita.com/yuiseki/items/bfa9ad984f7333377870)
[pySerial API](http://pyserial.readthedocs.io/en/latest/pyserial_api.html)
[python - pySerial 2.6: specify end-of-line in readline() - Stack Overflow](http://stackoverflow.com/questions/16470903/pyserial-2-6-specify-end-of-line-in-readline)