LoginSignup
5
9

More than 5 years have passed since last update.

【Python】【Windows】DLLを利用したPythonでのシリアル通信

Posted at

組み込みとかだとシリアル通信でデバッグするという事がしばしばあります。自作IoTデバイスとか作りますと、シェルなんてありませんから簡単なシリアルモニタの出番です。

その場合の定番はTeraTermになりますでしょうか。マクロも充実していますし。

ただ、シリアルの結果を加工したり、その結果をcsvにしようとか思うとPythonとか使いたくなるのです(私の場合。どれが良いとかじゃなく、単に慣れの問題っすね)。これ、pyserialを使うのがきっと王道なのだろうと思います。

が、

Javaとか挟まるのもなんか違和感が(何の?)。また別件でpythonとC言語くっつけて遊ぶ事してたので、それの応用で今回はシリアル通信を実現してみました。シリアル通信部分をDLLで作成して、それをctypesからアクセスします。組み込みぐらいの遅いメッセージ交換なら十分これで使えてます。

Windows独自の制御をPythonからどう使うかー・・みたいな参考になれば幸いです。

環境、制限など

以下で確認しています。あくまでも試作なのでもろもろご容赦戴きたい所です。

  • Windows10(64bit)、Windows7(64bit)で確認しました。
  • Pythonは2.7系32bitで、DLLもx86でビルドして使いました。
  • VisualStudio2015/2017 CommunityでDLLは作ってみました。
  • シリアルでは文字列のみ扱うという前提です。バイナリはこのままでは使えません(特にPython側)
  • 一回の最大通信サイズは1024バイトです。なのでちまちまやるイメージで。
  • 今回はCOMポートとBPSのみ設定可能で後はいつもの固定値です。だって3線クロスが基本ですよね・・とか書いてみる。(変更が必要なら簡単だと思いますので適当に改造してみて下さい)

ctypesについてごく簡単に

チュートリアルは以下
http://starship.python.net/crew/theller/ctypes/tutorial.html

VisualC++を使ったDLLはまぁ普通の感覚でコード書けるかと思います。一方python側は工夫が必要で、ctypesを使った関数定義などを行う必要があります。とはいえ上のチュートリアルと実際のコードを見た方が早いかと思いますので、早速コードを晒しちゃいます。

DLL側

以下がDLL側のコードになります。

#include "stdafx.h"
#include "stdio.h"
#include "windows.h"

#define DLL_API __declspec(dllexport)
#define WCHAR_PORT_NUMBER_BUF_LENGTH (16 * 2)
#define SERIAL_BUFFER_SIZE (1024)

extern "C" DLL_API INT32 __stdcall SERIAL_open(char *port, INT32 baud);
extern "C" DLL_API char * __stdcall SERIAL_read(void *len);
extern "C" DLL_API INT32 __stdcall SERIAL_write(char *text, INT32 len);
extern "C" DLL_API void __stdcall SERIAL_close(void);

static HANDLE SERIAL_handle = NULL;
static char SERIAL_recv[SERIAL_BUFFER_SIZE + 1];

DLL_API INT32 __stdcall SERIAL_open(char *port, INT32 baud)
{
    TCHAR tcPort[WCHAR_PORT_NUMBER_BUF_LENGTH];
    DCB comDcb;
    BOOL success;
    INT32 ret = 0;
    COMMTIMEOUTS comTimeouts;

    memset(tcPort, 0, 32);
    MultiByteToWideChar(CP_OEMCP,
        MB_PRECOMPOSED,
        port,
        strlen(port),
        tcPort,
        WCHAR_PORT_NUMBER_BUF_LENGTH / 2);

    SERIAL_handle = CreateFile(tcPort, GENERIC_READ | GENERIC_WRITE, 0, NULL,
        OPEN_EXISTING, 0, NULL);
    if (SERIAL_handle == INVALID_HANDLE_VALUE) {
        return -1;
    }

    success = SetupComm(SERIAL_handle, SERIAL_BUFFER_SIZE, SERIAL_BUFFER_SIZE);
    if (success == FALSE)
    {
        ret = -2;
        goto error;
    }

    memset(&comDcb, 0, sizeof(DCB));
    comDcb.DCBlength = sizeof(DCB);
    comDcb.BaudRate = baud;
    comDcb.fParity = FALSE;
    comDcb.Parity = NOPARITY;
    comDcb.ByteSize = 8;
    comDcb.StopBits = ONESTOPBIT;
    success = SetCommState(SERIAL_handle, &comDcb);
    if (success == FALSE) {
        ret = -3;
        goto error;
    }

    memset(&comTimeouts, 0, sizeof(COMMTIMEOUTS));
    comTimeouts.ReadIntervalTimeout = 500;
    comTimeouts.ReadTotalTimeoutMultiplier = 0;
    comTimeouts.ReadTotalTimeoutConstant = 500;
    comTimeouts.WriteTotalTimeoutMultiplier = 0;
    comTimeouts.WriteTotalTimeoutConstant = 500;
    success = SetCommTimeouts(SERIAL_handle, &comTimeouts);
    if (success == FALSE)
    {
        ret = -4;
        goto error;
    }

    success = PurgeComm(SERIAL_handle, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
    if (success == FALSE)
    {
        ret = -5;
        goto error;
    }

    return 0;

error:
    CloseHandle(SERIAL_handle);
    return ret;
}

DLL_API char * __stdcall SERIAL_read(void *len)
{
    BOOL  success;
    DWORD recvLen = 0;
    INT32 *lenToPython = (INT32 *)len;

    success = ReadFile(SERIAL_handle, SERIAL_recv, SERIAL_BUFFER_SIZE, &recvLen, NULL);
    if (success == FALSE || recvLen == 0) {
        *lenToPython = 0;
        return "";
    }

    *lenToPython = (INT32)recvLen;
    SERIAL_recv[recvLen] = '\0';
    return SERIAL_recv;
}

DLL_API INT32 __stdcall SERIAL_write(char *text, INT32 len)
{
    BOOL  success;
    DWORD dummy = 0;

    success = WriteFile(SERIAL_handle, text, strlen(text), &dummy, NULL);
    if (success == TRUE)
    {
        return 0;
    }
    else {
        return -1;
    }
}

DLL_API void __stdcall SERIAL_close(void)
{
    if (SERIAL_handle == NULL) {
        return;
    }
    CloseHandle(SERIAL_handle);
}

用意した関数は以下の4つ。極めて単純な作りです。

// ポートオープン、port="COMxx"、baudはbps。戻り値は0が成功で負数は失敗
INT32 SERIAL_open(char *port, INT32 baud)

// データ読み出し。呼び出すと lenには読み出したバイト数、charは受信データが入る。
// 500msecのタイムアウトで、*len=0の場合がありえます。
char * SERIAL_read(void *len)

// データ書き込み。textの文字列をlenバイト(コード見ると分かりますが最大1024bytes)
INT32 SERIAL_write(char *text, INT32 len)

//ポートクローズ
void SERIAL_close(void)

VisualStudioでDLLとして本コードをビルドします。その際の注意点を以下に。

  • VisualC++でWin32プロジェクト。DLLでプロジェクトを作成します。
  • で、上記のコードをそのプロジェクトで生成された空のcppにコピペします。
  • dllmain.cppとかはそのまま使います。
  • ちなみにPythonの環境とbit数を合わせて下さい(筆者は32bitで確認しました)
  • コードにもありますように、stdcallで各関数を定義しました。
  • defとか考えたくないので __declspec(dllexport)としています。

実際のコードはWin32APIを使った教科書通り(むしろ劣化版ですねごめんなさい)にシリアル通信を書いたつもりですきっと多分。。でビルドすると普通にdllが出来ます。このdllだけ使う感じでPython側を書きます。

プログラム作成時、以下のサイトが大変参考になりました。
http://www.geocities.co.jp/SiliconValley-SanJose/5309/serial.html
http://www.ys-labo.com/BCB/2007/070512%20RS232C%20zenpan.html

Python側(ctypesを利用したシリアル通信)

Python側です。上記のDLLを利用してシリアル通信を行うクラスのコードを以下に示します。ちなみにDLLのファイル名をserial_if.dllとした際のコードです。

#!/usr/bin/env python

from ctypes import *

'''
Describe serial DLL's functions.
'''

dll = windll.serial_if
dll.SERIAL_open.argtype = (c_char_p, c_int)
dll.SERIAL_open.restype = c_int
dll.SERIAL_read.argtype = c_void_p
dll.SERIAL_read.restype = c_char_p
dll.SERIAL_write.argtype = (c_char_p, c_int)
dll.SERIAL_write.restype = c_int

'''
Serial Device Driver
'''

class SerialDriver:
    def open(self, port, baud):
        int_ret = dll.SERIAL_open(port, baud);
        if int_ret == 0:
            return True
        else:
            return False

    def read(self):
         read_len = c_int(0)
         text = dll.SERIAL_read(byref(read_len))
         return text, read_len.value

    def write(self, text):
        write_ret = dll.SERIAL_write(text, len(text))
        if write_ret == 0:
            return True
        else:
            return False

    def close(self):
        dll.SERIAL_close();

DLLとのインターフェース

コメントで書いたDescribe serial DLL's functions.とある辺りがDLLを扱うための記述です。

今回は windllを使ってDLLを使う記述を採用しています。その際、まずは使うDLLの名前を以下のように設定します。

dll = windll.serial_if
# windll.(DLLのファイル名) で該当DLLを指定します。上記例はserial_if.dllを意味します

次にDLLでexportした各関数の引数と戻り値をctypesの記述を使って定義します。以下にCの関数との対比で書いてみます。


# INT32 SERIAL_open(char *port, INT32 baud);
dll.SERIAL_open.argtype = (c_char_p, c_int)
dll.SERIAL_open.restype = c_int

# char * SERIAL_read(void *len);
dll.SERIAL_read.argtype = c_void_p
dll.SERIAL_read.restype = c_char_p

# INT32 SERIAL_write(char *text, INT32 len);
dll.SERIAL_write.argtype = (c_char_p, c_int)
dll.SERIAL_write.restype = c_int

# void SERIAL_close(void);
# 引数、戻り値共にない場合は定義不要

CもPythonもやられた方でしたら、上記の対比で基本的な書き方は分かるかと思います。細かい型との対応はチュートリアルが参考になるでしょう。

後は定義した関数を普通に呼び出せます。以下注意点。特にポインタでCから値をもらう場合に注意が必要です。

  • pythonの型変換機能が効きます。実際textをc_char_pとかに突っ込んでも大丈夫でした。
  • ただし、引数にポインタを使う場合は参照渡しなので byrefを使います。
  • byrefを利用する場合ctypesで定義されたオブジェクトにする必要があるみたいです。上記コードではc_intで初期化してbyrefで入れています。
  • その場合、関数から戻った値を使う場合はvalueを使う必要があります。リストの例で言えばread_lenを戻すのではなくread_len.valueを戻しています。

上記のSerialDriverクラスはDLLの関数を呼び出してシリアル通信を行います。このクラスを利用する方はctypesを意識しなくても良いように実装されています。

valueの件とかは、stackoverflowの以下サイトが参考になりました。
http://stackoverflow.com/questions/2330587/how-to-convert-ctypes-c-long-to-pythons-int

Python側通信アプリの例

以下はSerialDriverクラスを使って実際のシリアル通信を行い、更にシリアルで受信したデータをログに残すプログラムです。ちなみにこれは、SerialDriverクラスをserial_lib.pyというファイル名で書いたという前提です(以下コードのimport見ればわかるかと思いますが)

#!/usr/bin/env python

from serial_lib import SerialDriver
from datetime import datetime
import sys
import time

def serial_read(serial, keyword):
    text = ""
    text_len = 0
    while text.find(keyword) < 0:
        read_text, read_len = serial.read()
        if read_len > 0:
            text_len += read_len
            text += read_text
    return text

def serial_test():

    filename = datetime.now().strftime('%Y%m%d%H%M%S') + ".txt"
    f = open(filename, "w")

    serial = SerialDriver()
    ret = serial.open("COM3", 115200)
    print "python:SERIAL_open=" , ret
    if ret == False:
        sys.exit()

    text = serial_read(serial,"teraterm command1")
    print text
    f.write(text)

    text = "python response1\r\n"
    serial.write(text)

    text = serial_read(serial,"teraterm command2")
    print text
    f.write(text)

    text = "python response2\r\n"
    serial.write(text)

    f.close()
    serial.close()

if __name__ == "__main__":

    serial_test()
    print "python: complete"

#EOF

また、筆者は本ファイルはserial_test.py として書きました。以降の説明もそれが前提となっています。

コードを見ても分かるかと思いますが、以下の関数から構成されます。

  • serial_test メイン処理
  • serial_readシリアル受信処理

DLLのコードを見ても分かりますが、500msecでタイムアウトしますので、受信結果が0バイトの事もあります。そのため、特定のキーワードを受信するまで読み続ける処理をここで行っています(なので、そのキーワードが来ないと抜けない仕組みになっています。これの対策はしてないですw)

メイン処理(serial_test 関数)では以下の流れで処理しています。

  1. ログファイルをオープン。ファイル名は「時刻.txt」としていまして、複数回実行しても別のファイルになるようにしています。
  2. シリアルポートをオープンします。
  3. 「teraterm command1」という文字列を受信するまで待ちます。
  4. 受信したらログファイルにその内容を書き込みます。
  5. 「python response1」を送信します。
  6. 「teraterm command2」という文字列を受信するまで待ちます。
  7. 受信したらログファイルにその内容を書き込みます。
  8. 「python response2」を送信します。
  9. ログファイル、及びシリアルポートをクローズします。

余談ですがCOM10以上だと上記ではダメで \\.\COM10" のように記載しないといけません(以下のようにMS様の仕様っすね)
https://support.microsoft.com/ja-jp/help/115831/howto-specify-serial-ports-larger-than-com9

通信相手の挙動(TeraTermマクロ)

上記のserial_test 関数の相手は試験的に以下のTeraTermのマクロで行いました。

timeout=30
sendln 'teraterm command1'
wait 'python response1'
sendln 'teraterm command2'

内容は以下の通りです。

  • タイムアウトは30秒で設定
  • 「teraterm command1」を送信
  • 「python response1」を受信するまで待つ
  • 受信したら、「teraterm command2」を送信する。

これでたがいに通信する感じです。

実行方法など

試験の実行は以下のようにして行いました。

  • DLLと、SerialDeriverクラス、及びそれを使った試験アプリ(筆者環境例でいうserial_test.py)は同じフォルダに入れておきます。
  • 対向機側とシリアルケーブル(クロス)で接続します。
  • 対向機側でTeraTermを起動、115200BPS, 8bit, no-parity, stop1bit で接続して上記マクロを実行します。
  • それから30秒以内に試験アプリを起動しますと、互いに通信をします。
  • また受信データはログファイルも残されます。

これを応用することでシリアル通信とデータ解析をPythonで行う事が可能です。シリアルを使った機能チェックを行い、それをCSV化するとか出来るようになります。センサデータシリアルで取って、機械学習だってきっとこれで出来るはず。

ライセンス

以下使わせて戴きました。素晴らしいソフトウェアを提供して下さり、ありがとうございます。

  • (一応書きます…)上記のコードはパブリックドメインとします。著作権を主張するほどのコードではないっつーことで。ただ当然ですが使用した際の損害は誰も請け負ってくれません。そこだけ注意で。
  • Python自体はPSF (Python Software Foundation)ライセンスです。
  • ↑の情報はWikipediaのPythonがソースです。
  • VisualStudioのライセンスホワイトペーパーはここで。Visual Studio Communityを使う場合は一応目を通した方がいいかも。

以上です。

5
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9