前回の続きで、いよいよ文字コードの入力とその送信である。文字コードをキーボードから入手するのはgets()となるが、実際に使うのは_getws_s()である。gets()は単にASCIIコードの入力で、これを拡張したWide Character用が_getws()、この_getws()のCRT(C RunTime)セキュリティ拡張型が_getws_s()となる。セキュリティ拡張というのは、例えばバッファアンダーラン(バッファオーバーフロー)の対策が施されている。今回の場合、_getws_s()に渡すunicodeBufのサイズは80Bytesにしているが、例えば以前の_getws()だとここに1KBとかの文字列を突っ込んでしまうとバッファがあふれてしまい、他の変数領域を破壊する可能性があった。これが_getws_s()だと、81Byte以上の文字列が渡されるとエラーを検出し、文字列を無理やり格納しようとしないでNULLを返す形で終了するから安全、という話である(ちなみに今回はサンプルなので、エラーが起きたらエラーメッセージだけを表示し、そのまま終了するが、このあたりはアプリケーションの要件に応じて必要ならエラーの状態をfrror()なりfeof()を使って確認、対処するといったアプローチになる)。

さて、ここで入力される文字列だが、昔のC言語とか(キーボード入力は無いけれど)Arduinoとかは、いわゆるASCIIコードなり日本語環境ならShift-JISコードで入ってくる。ところがWindows NT系列の場合、内部のコードは全てUnicodeで管理されているため、プログラム側もここにはUnicodeが入ってくることになる。勿論、PC内部だけで済むプログラムなら、単にWide Character対応の関数を呼び出すだけで済むのだが、相手がArduinoとなると、今度は明示的にASCIIコードで送信してやらないと、うまく受け取ることができない。これを行うのが、次にでてくるWideCharToMultiByte()である。この関数の使い方はこちらを見ていただくのが早いが、これを使ってUnicodeの文字コードをANSIコードページのコード(つまりASCIIコード)に変換するという具合だ。変換できない様な変なコードがキー入力されることは今回想定していないので、規定の文字コードはNULLとかにしている。

変換が正常に行われれば、変換後のバッファサイズが返ってくるから、ここは基本的に0でなければOKということで、0の場合のみエラーハンドリングをしている。ちなみに変数にcharOutLengthというものがあるが、こちらは変換後の文字サイズを入れるわけではなく(WideCharToMultiByte()の戻り値を受ければそういう動作になる)、続くWriteFile()が要求するので用意したというだけで、プログラム内部では今回利用していないものだ。

変換後の文字列は今度はansiBuf[]というバッファに格納されるので、最後にWriteFile()を呼び出し、これをCOMポートに送信する形だ。ここで注意するのは、例えばfputs()とかfputws()といった文字列書き込み関数を使ってはいけないこと。Windows NT系列だと、Unicodeが唯一認識できる文字列扱いなので、ASCIIコードはバイナリデータの扱いとなる。このため、プログラムでもバイナリデータの書き込みができるWriteFile()をわざわざ選んで使っているという仕組みだ。

ちなみに先ほどもちょっと触れたが、charOutLengthという変数はこのWriteFile()の4番目の引数"lpNumberOfBytesWritten"の要素として渡している。このWriteFile()は非同期I/O(ファイルの書き込み終了を待たずに関数が戻ってくる仕組み)をサポートしており、これを利用する場合は5番目の引数"lpOverlapped"に、書き込み終了をハンドリングするためのパラメータ(正確に書けば、I/O完了にあわせてシグナル状態になるイベント)を指定する。これを使う場合、WriteFile()を発行した直後はまだ実際の書き込みが終わっておらず、書き込みバイト数が0の場合もあるので、"lpNumberOfBytesWritten"を受け取らないという設定も可能である。ただこれをやった場合、"lpOverlapped"の指定が今度は必要になってしまう。非同期I/Oを使う場合、書き込みが完了するとWriteFile()によってEventが立ち上がる仕組みで、このEventの中で書き込みバイト数を受け取る事になり、遥かに大掛かりな仕組みが必要になる。今回のケースでは、別に5バイトかそこらの転送に非同期I/Oを使う必要もないので、素直にダミーでcharOutLengthという変数を定義、これを指定して書き込みバイト数を保存しつつ、それを見ないで放置という形で対処した。

さて、以上でプログラムも完成なので、ビルドして実行してみよう。起動後2秒ほどは反応が無いが、その後は入れた数字にあわせてLEDの点滅が変わる形だ。ちなみに受け取れる文字は、370回目で説明したとおり'0'~'D'の21文字で、4桁入力した後最後に小文字の'z'を入れて、Enterキーを押せばよい。

ということで、これが動いたらテストプログラムその2も完成である。

(続く)

List 1:

// SerialTest.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//

#include "stdafx.h"
#include <string.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define BUFSIZE 80
#define COMPORT _TEXT("COM6")
#define BAUDRATE 9600

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE      hCOM;       // COMポートアクセス用Handle
    wchar_t     unicodeBuf[BUFSIZE];// キーボード入力用バッファ(unicode)
    char        ansiBuf[BUFSIZE];   // 転送用バッファ(ansi)
    int         charInLength;   // 入力文字長
    unsigned long   charOutLength;  // 出力文字長

    // RS232CポートへのHandle作成
    hCOM = CreateFile(COMPORT,
              GENERIC_READ | GENERIC_WRITE,
              FILE_SHARE_READ | FILE_SHARE_WRITE,
              NULL,
              OPEN_EXISTING,
              0,
              NULL);
    if (hCOM == INVALID_HANDLE_VALUE)
    {
    CloseHandle(hCOM);
    _tprintf(_TEXT("COM port open error\n"));
    _exit(0);
    }

    // 現在のRS232Cポートの通信パラメータ取得
    DCB    dcbBuf;
    dcbBuf.DCBlength = sizeof(DCB);

    if (!GetCommState(hCOM, &dcbBuf))
    {
    CloseHandle(hCOM);
    _tprintf(_TEXT("COM port access error\n"));
    _exit(0);
    }

    // パラメータ設定
    dcbBuf.BaudRate = BAUDRATE; // 通信速度
    dcbBuf.ByteSize = 8;    // データ長
    dcbBuf.Parity = NOPARITY;   // パリティビット
    dcbBuf.StopBits = ONESTOPBIT;// ストップビット

    // RS232Cポートにパラメータ設定
    if (!SetCommState(hCOM, &dcbBuf))
    {
    CloseHandle(hCOM);
    _tprintf(_TEXT("COM port access error\n"));
    _exit(0);
    }

    // 2秒ほど待機
    Sleep(2000);

    while(1)
    {
    // stdinから1行読み込む
    if (!_getws_s(unicodeBuf, BUFSIZE))
    {
        // キーボード入力に失敗
        _tprintf(_TEXT("Key input error\n"));
        break;
    }
    charInLength = (int)_tcslen(unicodeBuf);

    // UnicodeをANSIに変換
    if (!WideCharToMultiByte(CP_ACP,
                 WC_NO_BEST_FIT_CHARS,
                 unicodeBuf,
                 charInLength,
                 ansiBuf,
                 sizeof(ansiBuf),
                 NULL,
                 NULL))
    {
        // 入力文字の変換に失敗
        _tprintf(_TEXT("Data convert error\n"));
        break;
    }

    // 入力された文字列をRS232Cポートに送る
    if( !WriteFile(hCOM, ansiBuf, charInLength, &charOutLength, NULL))
    {
        _tprintf(_TEXT("Data send error\n"));
        break;
    }
    }

    //後処理
    CloseHandle(hCOM);

    return 0;
}