今回のような環境測定システムのデータサーバとして、Ambient を利用する製作例が多い。もちろんそれでも全く問題ないんだけど、なるべく自己完結したシステムにするため、常時稼働している QNAP TS-128A をサーバにすることを試みた。
TS-128A に SSH でログインして、以下の作業を行う。
$ mkdir -p /share/Web/logs
$ vi /etc/config/apache/apache.conf
#(以下の行を追加)
Include /etc/config/apache/extra/apache-log.conf
$ vi /etc/config/apache/extra/apache-log.conf
#(新規作成)
# 864000 は「10日ごと」
CustomLog "|/usr/local/apache/bin/rotatelogs -l /share/Web/logs/access_log.%Y-%m-%d 864000" combined
ErrorLog "|/usr/local/apache/bin/rotatelogs -l /share/Web/logs/error_log.%Y-%m-%d 864000"
$ /usr/local/apache/bin/apachectl restart
以下の作業を行う。TS-128A はデフォルトで Python 2.7 がインストールされているので、これを使う。
$ mkdir -p /share/Web/iot/cgi-bin
$ vi /share/Web/iot/cgi-bin/.htaccess
Options +ExecCGI
AddHandler cgi-script .py
テスト用の CGI を動かしてみる。
$ vi /share/Web/iot/cgi-bin/index.cgi
#!/usr/bin/env python
print("Content-type: text/html\n")
print("<html>")
print("<head>")
print("</head>")
print("<body>")
print("It Worked!")
print("</body>")
print("</html>")
$ chmod +x /share/Web/iot/cgi-bin/index.cgi
http://192.128.1.12/iot/cgi-bin/index.cgi
にアクセスすると、無事 It Worked! と表示された。
サーバ側のCGIスクリプトは Python 2.7 で書く。骨格は下の通り。(Content-Type
行の最後の改行文字は1つだけ。python の print
文は改行を1つ出力するので、文字列中の '\n'
とprint
文が出力する改行で、正しくレスポンスヘッダが終了する。)
import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "") # 複数の値が送られてきたとき最初のものを使う
print('Content-Type: text/html; charset=utf-8\n')
print(...) # 出力する本体
Mac上でスクリプトを開発するときは、適当なフォルダ中に cgi-bin
というフォルダを作成して、そのフォルダ(cgi-binの1つの上のフォルダ)上で以下のコマンドを実行する。スクリプトに+x属性をつけるのを忘れないように。
$ python -m CGIHTTPServer [port]
ポート番号のデフォルトは8000。
Mac上のスクリプトにPOSTリクエストを送って試すときは、curlを使う。
$ curl [-X POST] -d "key1=value1" -d "key2=value2" http://localhost:8000/cgi-bin/test.py
または
$ curl [-X POST] -d "key1=value1&key2=value2" http://localhost:8000/cgi-bin/test.py
TS-128A上のファイルの配置は以下のようにする。データ保存ディレクトリの "1" というのは、データのチャンネルを複数用意して、複数のデバイスから独立にデータを送信できるようにするためのもの。
/share/Web/iot/cgi-bin/post.py
/share/Web/iot/cgi-bin/show.py
/share/Web/iot/cgi-bin/1/logs
CGIの実行ユーザーは httpdusr
なので、/share/Web/iot/cgi-bin/1/logs
以下のファイル、ディレクトリはhttpdusr
所有にしておく必要がある。
データを受け取ってログファイルに保存する。ファイルアクセスの競合を避けるため、ロック用のファイルを作成して、fcntl.flock()
で排他的ロックをかける。
#!/usr/bin/env python
import cgi
import sys
import os
import fcntl
import re
from datetime import datetime
form = cgi.FieldStorage()
a0 = form.getfirst("a0", "0")
a1 = form.getfirst("a1", "0")
a2 = form.getfirst("a2", "0")
temp = form.getfirst("temp", "0")
humid = form.getfirst("humid", "0")
ch = form.getfirst("ch", "1")
verbose = form.getfirst("v", "")
comment = form.getfirst("comment", "")
if "SCRIPT_FILENAME" in os.environ:
full_path = os.environ["SCRIPT_FILENAME"]
else:
full_path = os.environ["PATH_TRANSLATED"] + os.environ["SCRIPT_NAME"]
dir_path = re.sub(r"\/cgi-bin\/(\w+)\.py$", "/", full_path)
log_dir = dir_path + ch + "/logs/"
lock_file = log_dir + "flock"
today = datetime.today()
daystring = "{:04d}{:02d}{:02d}".format(today.year, today.month, today.day)
timestring = today.strftime("%Y/%m/%d %H:%M:%S")
# Update log
with open(lock_file, "w+") as lockf:
fcntl.flock(lockf.fileno(), fcntl.LOCK_EX)
try:
log_file = log_dir + daystring + ".log"
with open(log_file, "a") as logf:
if a2 == "0" and comment != "":
# Write comment only
logf.write("# {} at {}\n".format(comment, timestring))
else:
if comment != "":
comment_str = " # " + comment
else:
comment_str = ""
logf.write("{} {} {} {} {} {}{}\n".format(timestring, a0, a1, a2, temp, humid, comment_str))
finally:
fcntl.flock(lockf.fileno(), fcntl.LOCK_UN)
print('Content-Type: text/html; charset=utf-8\n')
if verbose != "":
print('<body>')
if comment != "":
print('<p>comment: {}</p>'.format(comment))
else :
print('<p>a0: {}</p>'.format(a0))
print('<p>a1: {}</p>'.format(a1))
print('<p>a2: {}</p>'.format(a2))
print('<p>temp: {}</p>'.format(temp))
print('<p>humid: {}</p>'.format(humid))
print('</body>')
最低限の機能しか実装してない。とりあえず困ってないのでこれで運用している。必要に応じて拡張する予定。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cgi
import sys
import os
import fcntl
import glob
import re
from datetime import datetime
from datetime import timedelta
form = cgi.FieldStorage()
start = form.getfirst("start", "")
end = form.getfirst("end", "")
ch = form.getfirst("ch", "1")
if "SCRIPT_FILENAME" in os.environ:
full_path = os.environ["SCRIPT_FILENAME"]
else:
full_path = os.environ["PATH_TRANSLATED"] + os.environ["SCRIPT_NAME"]
dir_path = re.sub(r"\/cgi-bin\/(\w+)\.py$", "/", full_path)
log_dir = dir_path + ch + "/logs/"
lock_file = log_dir + "flock"
if end == "":
endtime = datetime.today()
else:
endtime = datetime.strptime(end, "%Y%m%dT%H%M%S")
if start == "":
starttime = endtime - timedelta(days = 7)
else:
starttime = datetime.strptime(start, "%Y%m%dT%H%M%S")
startday = datetime(starttime.year, starttime.month, starttime.day)
endday = datetime(endtime.year, endtime.month, endtime.day)
start_str = starttime.strftime("%Y/%m/%d %H:%M")
end_str = endtime.strftime("%Y/%m/%d %H:%M")
lines = []
first_str = None
last_str = None
oldest_str = None
newest_str = None
# Read log
with open(lock_file, "w+") as lockf:
fcntl.flock(lockf.fileno(), fcntl.LOCK_EX)
try:
files = glob.glob(log_dir + "*.log")
files.sort()
last_f = None
read_start_f = None
first_f = None
for f in files:
m = re.search(r"\/(\d\d\d\d)(\d\d)(\d\d)\.log", f)
if m:
year = m.group(1)
month = m.group(2)
day = m.group(3)
if first_f is None:
first_f = f
newest_str = "{}/{}/{} 23:59:59".format(year, month, day)
if oldest_str is None:
oldest_str = "{}/{}/{} 00:00:00".format(year, month, day)
if newest_str >= start_str + ":00" and read_start_f is None:
read_start_f = last_f
last_f = f
if newest_str is None:
newest_str = None
oldest_str = None
if read_start_f is None:
read_start_f = first_f
m = re.search(r"\/(\d\d\d\d)(\d\d)(\d\d)\.log", read_start_f)
day = datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)))
while day <= endday:
daystring = "{:04d}{:02d}{:02d}".format(day.year, day.month, day.day)
day_log = log_dir + daystring + ".log"
if os.path.exists(day_log):
with open(day_log, "r") as logf:
while True:
line = logf.readline()
if not line:
break
line = line.strip()
if line[0:1] == "#":
continue
a = line.split()
lines.append("[\"{} {}\", {}, {}, {}, {}, {}],\n".format(*a))
last_str = "{} {}".format(a[0], a[1])
if first_str is None:
first_str = last_str
day = day + timedelta(days = 1)
finally:
fcntl.flock(lockf.fileno(), fcntl.LOCK_UN)
variable = " var list = [\n"
variable += "\n".join(lines)
variable += " ];\n"
variable += " var start_str = \"" + start_str + "\";\n"
variable += " var end_str = \"" + end_str + "\";\n"
variable += " var oldest_str = \"" + (oldest_str or first_str) + "\";\n"
variable += " var newest_str = \"" + (newest_str or last_str) + "\";\n"
print('Content-Type: text/html; charset=utf-8\n')
html = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>家庭菜園データログ</title>
<link rel="stylesheet" href="../style.css" type="text/css" />
</head>
<body onload="draw_graph(start_t, end_t);">
<script type="text/javascript">
''' + variable + '''
</script>
<script type="text/javascript" src="../show.js">
</script>
<p id="header">
</p>
<div class="canvas-wrapper" style="height:480px">
<canvas id="graph" width="640" height="480"></canvas>
<canvas id="graph2" width="640" height="480"></canvas>
</div>
<div style="font-family:sans-serif; font-size:10pt;">
<span id="time1"></span>
<br />
<span id="value1" style="color:blue;"></span>
<span id="value2" style="color:red;"></span>
<span id="value3" style="color:blue;"></span>
<span id="value4" style="color:red;"></span>
<span id="value5" style="color:blue;"></span>
</div>
<form>
<input type="button" id="last" value=" < " onclick="do_last();" />
<input type="button" id="next" value=" > " onclick="do_next();" />
<input type="button" id="expand" value="ズームイン" onclick="do_expand();" />
<input type="button" id="shrink" value="ズームアウト" onclick="do_shrink();" />
</form>
</body>
</html>
'''
print(html)
こんな感じで稼働しています。