SQLite3(2)

Raspberry Piと直接的には関係無い話だが、前回SQLite3モジュールを使って簡単なデータベースプログラムを作ったときに躓いた箇所があったので、記しておく。

最初に作ったプログラムはデータベースを開いてテーブル作成する部分が、次のようになっていた。

class Measure(threading.Thread):
    def __init__(self, dbname):
        threading.Thread.__init__(self)
        self.con = sqlite3.connect(dbname,
                            isolation_level = None,
                            timeout = 5000)
        self.daemon = True
    def run(self):
        try:
            sql = "create table data(date text primary key, real temp);"
            self.con.execute(sql)
        except Exception, ex:
            print ex

拡張する事を考えてデータベース操作はスレッドに分離して、スレッド初期化の時にデータベースへのコネクションも作成するようにしてあったのだが、実行すると以下のようなエラーが出てしまう。

SQLite objects created in a thread can only be used in that same thread.The object was created in thread id -1225788912 and this is thread id –1231440784

SQLiteのオブジェクトは作成したスレッド内でしか使えない、というエラーだ。調べてみると確かにSQLiteのコネクションはスレッド間で共有できないと書かれていた。

確認のためプログラムのメイン、スレッドの初期化、スレッドのRUNメソッドのそれぞれで、スレッドの識別IDを表示するようにしてみた。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import smbus
import time
import datetime
import threading
import sqlite3

class Measure(threading.Thread):
    def __init__(self, dbname):
        threading.Thread.__init__(self)
        self.con = sqlite3.connect(dbname,
                            isolation_level = None,
                            timeout = 5000)
        self.daemon = True
        print "init:",threading.currentThread().ident
    def run(self):
        print "run: ",threading.currentThread().ident
        try:
            sql = "create table data(date text primary key, real temp);"
            self.con.execute(sql)
        except Exception, ex:
            print ex

if __name__ == '__main__':
    print "main:",threading.currentThread().ident
    measure = Measure('temperature.db')
    measure.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print '\nbreak'

その結果は以下のようになった。

main: -1225342448
init: -1225342448
run:  -1230994320
SQLite objects created in a thread can only be used in that same thread.The object was created in thread id -1225342448 and this is thread id -1230994320

スレッドの__init__とrunは確かに異なる識別IDになっている。
スレッドの初期化と本体が異なるスレッドなのは当たり前なのだが、普段あまり意識していなかったのでちょっと引っかかってしまった。

複数のスレッドでコネクションを共有できないのは不便な気もするが、同じデータベースに対して複数のコネクションからアクセスするのは大丈夫らしい。同じスレッドの中で作成したコネクションしか使えない、という事だけ注意しておけばRaspberry Piでの使用はそれほど問題にはならないだろう。

広告

SQLite3

I2C接続の温度センサーモジュールを使って、現在の温度を表示するだけのアプリは簡単に作ることができたのだが、それだけならばRaspberry Piを使用する必然性は殆ど無く、I2Cインタフェースを持つ8ビットマイコンでも事足りてしまう。

Raspberry PiはワンボードマイコンでありながらOSが動作する性能を持っていて、メモリやストレージのリソースも8ビットマイコンとは比べものにならないほど潤沢に使えるので、ArduinoとPCを組み合わせて行っていたような用途の一部、たとえば大量の計測データをデータベースで処理するような事が単体で行える。

RaspbianにインストールされているPythonには、軽量・高速なデータベースSQLite3を使うためのライブラリが標準で入っているので、それを利用すれば別にSQLサーバをインストールしなくても手軽にデータベースを扱う事ができる。

ここでは温度センサーモジュールから計測した温度を、計測日時のデータと共に一分間隔でデータベースに記録するようにしてみた。データベース名は「temperature.db」で、SQLite3はデータベースを構成するファイルが一個だけなのでデータベース名とファイル名は常に同じになる。

データベースのテーブル「data」には計測日時と温度の二つのカラムが存在するが、SQLite3には日付を表すデータ型が存在しないため、計測日時はテキスト型のデータになっている。

プログラムは以下の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import smbus
import time
import datetime
import threading
import sqlite3

class i2c:
    def __init__(self, bus, addr):
        self.b = smbus.SMBus(bus)
        self.addr = addr
    def put(self, cmd, data):
        self.b.write_byte_data(self.addr, cmd, data)
    def get(self, cmd):
        return self.b.read_byte_data(self.addr, cmd)
    def getblock(self, cmd, len):
        return self.b.read_i2c_block_data(self.addr, cmd, len)

class ADT7410(i2c):
    def __init__(self):
        i2c.__init__(self, 1, 0x48)
    def gettemp(self):
        result = i2c.getblock(self, 0x00, 2)
        temp = (result[0] << 8 | result[1]) >> 3
        if(temp >= 4096):
            temp -= 8192;
        return temp / 16.0

class Measure(threading.Thread):
    def __init__(self, dbname):
        threading.Thread.__init__(self)
        self.dbname = dbname
        self.daemon = True
    def run(self):
        con = sqlite3.connect(self.dbname,
                              isolation_level = None,
                              timeout = 5000)
        try:
            sql = "create table data(date text primary key, real temp);"
            con.execute(sql)
        except Exception, ex:
            pass
        dev = ADT7410()
        past = datetime.datetime.now()
        while True:
            now = datetime.datetime.now()
            if(past.minute != now.minute):
                temp = dev.gettemp()
                sql = "insert into data values('" + \
                        now.strftime("%Y/%m/%d %H:%M:%S") + \
                        "',%.2f" % temp + ");"
                print("current temperature -> %7.2f" % temp)
                try:
                    con.execute(sql)
                except sqlite3.Error as ex:
                    print ex
            past = now
            time.sleep(0.5)

if __name__ == '__main__':
    measure = Measure('temperature.db')
    measure.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print '\nbreak'

データを記録するだけのプログラムなので、記録されたデータを確認するためにはコマンドラインで動作するツールが必要となる。ツールのインストールは

$ sudo apt-get install sqlite3

で行えば良い。コマンドラインツールはsqlite3の後ろにデータベース名を書いて起動する。計測するプログラムを一定時間動かしてから、コマンドラインツールでSQL文を実行すると、テーブル「data」に日時と温度が格納されている事が確認できるはず。

$ sqlite3 temperature.db
SQLite version 3.7.13 2012-06-11 02:05:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from data;
2013/07/04 23:08:00|29.5
2013/07/04 23:09:00|29.5
2013/07/04 23:10:00|29.56
2013/07/04 23:11:00|29.56
2013/07/04 23:12:00|29.56
2013/07/04 23:13:00|29.56
2013/07/04 23:14:00|29.62
sqlite> .exit
$

今回はデータを蓄積するだけだが、データベースを使えばデータを絞り込んだり並べ替えたりといった事も容易にできる。同じような事を8ビットマイコン単体で行うのはかなり大変なので、この手の用途にならRaspberry Piを使う意味が出てくるだろう。