前回の続きで、いよいよ文字コードの入力とその送信である。文字コードをキーボードから入手するのは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;
}