家庭菜園ロガーの製作:ソフトウェア(サーバ側)

サーバ環境

 今回のような環境測定システムのデータサーバとして、Ambient を利用する製作例が多い。もちろんそれでも全く問題ないんだけど、なるべく自己完結したシステムにするため、常時稼働している QNAP TS-128A をサーバにすることを試みた。

Apache の設定:ログの保存

 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

Apache の設定:CGIの設定

 以下の作業を行う。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 の設計

サーバ側の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" というのは、データのチャンネルを複数用意して、複数のデバイスから独立にデータを送信できるようにするためのもの。

 CGIの実行ユーザーは httpdusr なので、/share/Web/iot/cgi-bin/1/logs 以下のファイル、ディレクトリはhttpdusr 所有にしておく必要がある。

データポスト用のCGI

 データを受け取ってログファイルに保存する。ファイルアクセスの競合を避けるため、ロック用のファイルを作成して、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>')

データ表示用のCGI

 最低限の機能しか実装してない。とりあえず困ってないのでこれで運用している。必要に応じて拡張する予定。

#!/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)

 こんな感じで稼働しています。