Etizolam

For our good night sleep.

温度センサーDHT11をRaspberry piで使う(C言語でpython拡張)

C言語を使ってPython拡張についてもう少し…

今回の事例のようにC言語でPythonの拡張するには、次のような順序で作業を進めていくことなると思います。(当たり前か…)

1. C言語でのアプリケーション開発。
2. Pythonから扱えるようにするための、C言語ソースへのラッピンコード追記 & setup.pyの追加。
3. コンパイルとテスト。

第1,第3ステップは、一般的な開発と大差がないので今回のスコープ外ということにし第2ステップに注目して解説を進めていくことにします。

尚、Raspberry Piの場合は、ビルド環境も簡単にインストールできるので実機でビルドしながら開発を進めるという方法をとるのがいいと思います。C言語のソースのサイズにもよると思いますが、今回のようにセンサーを初期化する関数と計測した値を読み込む関数の2つ程度ならビルド時間も許容できるのではないかと思います。

(実開発はしないので、次の開発環境準備部分は読み飛ばしても大丈夫です。)

まずは、apt-getコマンドで、gcc及び一般的な開発環境をインストールします。

1
sudo apt-get install pyhon-dev build-essential

次に、GPIOを制御しているBCM2835のheaderファイルもインストールします。 インストール方法に関しては、C library for Broadcom BCM 2835 as used in Raspberry Piのドキュメントを参考にcm2835-1.36.tar.gzをDLし、次のようにビルド&インストールします。

1
2
3
4
5
6
tar zxvf bcm2835-1.xx.tar.gz
cd bcm2835-1.xx
./configure
make
sudo make check
sudo make install

尚、bcm2835のheaderファイルで使われている各モジュールの詳細はhttp://www.airspayce.com/mikem/bcm2835/modules.htmlを参照してください。(大元の資料が一番ですよね…)

ここれで、Raspberry Pi上でのPython拡張モジュールの開発の準備は完了です。

C言語ソースへのラッピンコード追記

C言語のコードを拡張していくには次の4項目を書き足すことになります。

1. Python用のheaderファイル宣言 (#include <Python.h>)
2. モジュールに含む関数へのPythonラッパー (static PyObject * モジュール名_関数名()の部分)
3. モジュール内の関数のリスト (static PyMethodDef モジュール名Methods[]の部分)
4. モジュールをイニシャライズする関数 (void initModule()の部分)

前回のポストで紹介したadafruitが提供してくれているリポジトリの中のAdafruit_DHT_Driver_Pythonディレクトリにある、dhtreader.cというファイルを項目に従って見ていくことにします。

1. Python用のheaderファイル宣言

29行目で、ヘッダーファイルの宣言をしていますね。

1
29 #include <Python.h>

2. モジュールに含む関数へのPythonラッパー

128~132行目にかけてPython環境からC言語のbcm2835に初期化のできるようPythonのラッパーを書いていますね。

ここでPyObjectを定義し、モジュール名と関数名を”_”(アンダースコア)で繋げて書くことで、当該モジュールをインポートした時にPython Scriptからdhtreader.init()みたいな方法で実行できるようにしています。

1
static PyObject * [モジュール名]_[関数名](PyObject *self, PyObject *args)

Pythoラッパーの一般的な役割は、Pythonの値を受け取ってCの値に変換し、Cの適切ば関数を実行することです。そして、Cの関数が実行された後でCの値をPythonの値に戻してあげることです。

CからPythonには、Py_BuildValue()を使って、単体値かタプル形式のPythonオブジェクトを戻すことができます。

1
2
3
4
5
128 static PyObject *
129 dhtreader_init(PyObject *self, PyObject *args)
130 {
131     return Py_BuildValue("i", bcm2835_init());
132 }

Py_BuildValue()では、第1引数の文字のフォーマットに合わせて、第2引数の値をオブジェクトに変換します。

フォーマット文字は、次の表の仕様になっています。

[フォーマット文字]   [Pythonタイプ]       [C/C++ タイプ]
s, s# str/unicode, len() char*(, int)
z, z# str/unicode/None, len() char*/NULL(, int)
u, u# unicode, len() (Py_UNICODE*, int)
i int int
b int char
h int short
l int long
k int or long unsigned long
I int or long unsigned int
B int unsigned char
H int unsigned short
L long long long
K long unsigned long long
c str char
d float double
f float float
D complex Py_Complex*
O (any) PyObject*
S str PyStringObject
Nb (any) PyObject*
O& (any) (any)

先のPy_BuildValue()例では、bcm2835_init()の結果がint値になるので、iを指定してPythonオブジェクトに戻しています。

さて、次のブロックでは、Pythonから受け取った値をCで使える値に変換している部分に注目します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
134 static PyObject *
135 dhtreader_read(PyObject *self, PyObject *args)
136 {
137     int type, dhtpin;
138
139    if (!PyArg_ParseTuple(args, "ii", &type, &dhtpin))
140        return NULL;
141
142    float t, h;
143    int re = readDHT(type, dhtpin, &t, &h);
144
145    if (re == 0) {
146        return Py_BuildValue("(d,d)", t, h);
147    } else if (re == -1) {
148 #ifdef DEBUG
149        printf("sensor read failed! not enough data received\n");
150 #endif
151    } else if (re == -2) {
152 #ifdef DEBUG
153        printf("sensor read failed! checksum failed!\n");
154 #endif
156    }
157
158    return Py_BuildValue("");
159 }

139行目のPyArg_ParseTuple()とある部分がPythonから渡ってきた値をCの値に置き換えています。この関数でフォーマット文字の列をつかって渡ってくる値を形式を指定し変換します。

計測には、C言語で書かれたソースの前半(31~91行)で指定しているreadDHT()関数を使い、結果はPy_BuildValue()で、floatの数値をタプルとしてPythonオブジェクにしているのが分かりますね。

それ以外のDEBUG用のコードは、readDHT()の戻り値によって、プリントディバッグできるようになっているのでしょうね…。

3. モジュール内の関数のリスト

次の部分のstatic PyMethodDef DHTReaderMethods[]ではモジュールをimportした後に、PythonインタプリターがそれぞれのメソッドとCの関数との対応表を提供しています。

DHTReaderMethodsは、関数リストを定義するための関数の名前です。モジュール名にMethodsを続けて書きます。この名前は、次の初期化のブロックでインタープリタに伝える関数リストの指定しに使います。

1
2
3
4
5
6
7
161 static PyMethodDef DHTReaderMethods[] = {
162    {"init", dhtreader_init, METH_VARARGS,
163     "initialize dht reader"},
164    {"read", dhtreader_read, METH_VARARGS,
165     "temperature and humidity from sensor"},
166    {NULL, NULL, 0, NULL}        /* Sentinel */
167 };

内容は、次の通りです。

1
{"[Pythonメソッド名]", [Cコード内の関数名], [Pythonから渡す引数の形式指定], "[解説]"}

尚、[Pythonから渡す引数の形式指定]の指定には、METH_VARARGSMETH_KEYWARDがあります。(METH_KEYWARDについては、PyArg_ParseTupleKeywards()とのセットで指定するは分かっているのですが詳細な用法が…。)

169行のNULLのは、リストの終わりを示す記号になります。

1
{NULL, NULL, 0, NULL}

4. モジュールをイニシャライズする関数

最後の部分は、モジュールがインポートされた時にPythonインタープリタによって実行される関数です。

1
2
3
4
5
6
7
8
9
169 PyMODINIT_FUNC
170 initdhtreader(void)
171 {
172    PyObject *m;
173
174    m = Py_InitModule("dhtreader", DHTReaderMethods);
175    if (m == NULL)
176        return;
177 }
1
Py_InitModule("[モジュール名]", [モジュール内の関数を指定したC内の関数])

以上で、dhtreader.c内のコードでpython拡張に関わる部分はの解説は全てです。

試しに取得しているレポジトリーのAdafruit_DHT_Driver_Pythonディレクトリで、次のコマンドを実行すると拡張モジュールがビルドされているはずです。~/Adafruit-Raspberry-Pi-Python-Code/Adafruit_DHT_Driver_Python/build/lib.linux-armv6l-2.7以下を見てみてください。(自分で書いているわけではないので、できるに決まってますよね〜)

1
python setup.py build

感想:

調べてみて感じたことは、「CソースからPython拡張モジュールにするための追記自体はそれほど複雑ではない」ということです。僕にとっては、むしろ元のCソースの処理を理解し、通信の方式やバイナリをCでハンドリングする部分を理解するのに頭を使ったような気がします。

今後モノのインターネットが進むと、Pythonでのプロトタイピングなんてケースが多々出てくると思います。Cとの兼ね合いでちょっと困ったという時には、この手法は十分使えるのではないでしょうか。Python本家のドキュメントを一読しておくと、いざという時に慌てなくすむかもしれませんね。

みちくさ:

僕は、未だ試していませんが、py-libbcm2835というCtypesのバインディングもあるようです。そちらも合わせて参考にしてみてください。