Apple原理主義者の大坪です。何故Apple原理主義者がC#を使うか?私は狂信的なApple原理主義者ですが、現実主義者でもあるのです。必要があればなんでも使ってやろうじゃないの。
とはいえ
最近昔を思い返すことが多い。当時は新しい環境に移る時まず本を買ったものです。しかし最近はGoogle先生にお伺いをたてればあれこれ情報が手に入る。とはいえ新しい環境に移るときは
「そもそも何のキーワードで探せばよいのか」
という問題にぶつかる。WindowsのPC用アプリケーションってなんて呼ばれているのか。(Macなら"Mac OS X Application"ですむのですが)
というレベルなので、今回書く内容は多分最適とか正解からはほど遠い物で、「今とりあえずこれで動いている」というものです。おまけにあれやこれやの理由から動くプロジェクトではなく、必要部分のソースだけを出しますがご容赦ください。
さて
試行錯誤の末まずたどり着いたのが"WinForm"なるキーワードです。しばらくそれを使ってあれこれやっていたのですが、どうも様子がおかしい。調べてみると最近はそれがWPFなるものに変わったらしい。でもって「ザムル」とかいろいろ恐ろしい言葉が並ぶ訳です。いつのまにかGUIはXMLで定義し、その背後のコードと分離するやり方が主流になったのだな、、などとXCodeで作る際にもInterface Builder(年がばれるか?)を使わない人間としては感慨に耽るのでした。
などとあれこれ言っている場合ではない。画面がなんとかできると、今度はC++で開発されたDLLライブラリの中の関数をC#からコールし、シリアル通信を行う必要があるのです。その部分のコード(一部)を記事の末尾に示します。ちなみにここに示したコードは「駄目なコードでもないよりはマシ。少なくとも出発点として使ってもらえるだろう」ポリシーの元公開しておりますので、左様ご承知頂きたく。
このコードは、ソーバル社のHRW-1000というRFIDリーダーからカードの情報を読み取るためのものです。ドライバはTRW社のサイトからダウンロードできます。ここからプロトコル仕様に従って、ごりごり通信を行うこともできるのでしょうか、せっかくWindowsで開発しているので、ソーバル社から提供されているDLLライブラリを利用して通信します。
やりたいことはなにか?言葉で書けば
・デバイスをオープンしてCOMポートで通信できるようにする
・「カードを読み取ってね」というコマンドをbyte列にしてリーダーに送信する
・読み取った結果を、byte列としてリーダーから受信する。
明確だし、簡単に聞こえますね。しかしこうしたハードウェアに近いところの処理というのは、その環境特有のあれこれをしないと動いてくれない。Apple原理主義者がつまづいた点を以下に列挙します。
- C#からC++用のDLLを呼ぶ時には、DLLImportというものを使わなくてはならない。
それまでもC#用のDLLを使う事があったのですが「参照の追加」をすればなんとかなりました。しかしC++用のDLLではそれだけではだめ。DLLImportで場所を指定する必要があります。
DllimportでGoogle先生にお伺いを立てると、シンプルな例がいくつもでてきます。そうかそうか、と
素直に
DLLImport("rfidsobal.dll);
と書いたのはだめで、ここに書いたように"CallingConvention = CallingConvention.Cdecl"とパラメータを指定する必要があるようです。なぜかというと、このDLLが「アンマネージドコード」と呼ばれるものであるため。この場合はWin32.APIを用いているようです。
- ライブラリとのパラメータ変換
次の問題はパラーメータの型変換です。メインのプログラムはC#で、DLLはC++用のもの。そりゃ言語も違うから型も違うだろう。ではどうしよう、とGoogle先生に聞いてみるとこんなページがヒットする。とても解りやすくかかれているのですが、ここだけ観ていればOKというほど話はストレートにいかない。
今回使うAPIでは受信データをcharにいれてくれることになっています。なるほど、ではcharは、、、そうかSystem.Text.StringBuilderにすればいいんだ、とやってみると何故か最初の一文字しか受信できない。これは困った。
後から考えれば当たり前の話で、APIではchar*になってますけど、具体的にはbyteでデータをつめて返してくれるわけです。それはもちろん文字列ではない。しかしStringBuilderで受けると「ここに入ってるのは、当然文字コードなんだよねー」と「解釈」されてしまいます。結果として、どうやっても最初の一文字しか読み取れない。(2byte目にnullがあったんだと思います)
ではどうするか、というわけでまたあちこちさがしてたどり着いたのがprivate void checkResult()に書いたような方法です。
多分これはbyte配列をそのまま受け取るようなことをしているのだと。これでめでたくデータ受信ができました。
では私はなぜApple原理主義者としての矜持を捨ててまでWindowsでプログラムを書き、RFIDリーダーと格闘しているか。その理由についてはいつかお話する機会もあろうかとおもいます。
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Runtime.InteropServices; | |
//Sobal社製HS-1000Mを接続し、C#でシリアル通信を行うためのサンプル | |
//説明に必要な部分しか掲載していません。 | |
namespace SomeName | |
{ | |
class CardReader | |
{ | |
private int commNo; | |
//使用するDLLの指定宣言;アンマネージドコードのため、CallingConventionがないと正常に動作しない | |
[DllImport("rfidsobal.dll", CallingConvention = CallingConvention.Cdecl)] | |
//デバイスオープン用の関数 | |
extern static int so_comm_setup(System.IntPtr pWnd, int iType, int iPort, long lSpeed); | |
//カード読み取り要求送信用の関数 | |
[DllImport("rfidsobal.dll", CallingConvention = CallingConvention.Cdecl)] | |
extern static int so_send(int iCommNo, string pcCmd, int iLen); | |
//カード読み取り結果受信用の関数 | |
[DllImport("rfidsobal.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] | |
extern static int so_recv(int iCommNo, IntPtr pcCmd, ushort usTimeOut); | |
//デバイスをオープンする | |
public void openDevice() | |
{ | |
commNo = so_comm_setup(IntPtr.Zero, 1, 3, 115200L); | |
Console.Write("result = {0}", commNo); | |
} | |
//カード読み取り要求を送信。 | |
private void sendRequest() | |
{ | |
byte[] message = new byte[1024]; | |
message[0] = 0x53; | |
message[4] = 0x20; | |
message[6] = 0x02; | |
message[8] = 0x20; | |
message[9] = 0x14; | |
message[10] = 0x01; | |
message[11] = 0x28; | |
message[12] = 0x08; | |
message[13] = 0x58; | |
message[16] = 0x01; | |
message[18] = 0x33; | |
int result = so_send(commNo, Encoding.ASCII.GetString(message), 19); | |
} | |
//読み取り結果の取得 | |
private void checkResult() | |
{ | |
byte[] message = new byte[1024]; | |
//ここをlpBufferの代わりにStringBuilderを使うと正常に読み込めない | |
IntPtr lpBuffer = Marshal.AllocHGlobal(1024); | |
//resultには読み込んだバイト数が入っている(らしい) | |
int result = so_recv(commNo, lpBuffer, 1000); | |
if (result == 0) return; | |
byte[] byteData = new byte[result]; | |
for (int i = 0; i < result; i++) | |
{ | |
byteData[i] = Marshal.ReadByte(lpBuffer, i); | |
} | |
//取得したbyte[]の解析。内容は省略 | |
this.analyzeResult(byteData); | |
Marshal.FreeHGlobal(lpBuffer); | |
lpBuffer = IntPtr.Zero; | |
} | |
} | |
} |