C# では、SerialPort
クラスを用いてシリアルポートでの通信を行うことができる。
しかし、このクラスには、扱うポートの名前にこだわりがあり、ポートが気に入らない名前だというだけで開かずに例外を吐くという仕様がある。
そこで、この記事では、そのようなこだわりによって生じる害を少し軽減する方法を紹介する。
システムに存在するポートを列挙する
SerialPort.GetPortNames()
関数を用いると、システムに存在するシリアルポートを列挙できる。
using System;
using System.IO.Ports;
class ListPorts
{
public static void Main(String[] args)
{
string[] ports = SerialPort.GetPortNames();
Console.WriteLine(string.Join("\n", ports));
}
}
YUKI.N>ListPorts
CNCA0
CNCB0
COM5
COM6
ここで表示されている CNCA0
および CNCB0
は、Null-modem emulator (com0com) で作成した仮想シリアルポートである。
たとえば、RS232Cテストツール を用いると、これらのポートで通信ができる。
シリアルポートで通信を行う
SerialPort
クラスを用いた通信の基本的な流れは、以下のようになる。
-
SerialPort
クラスのオブジェクトを作成する - 必要に応じて、パラメータ (フロー制御の方式など) を設定する
-
Open()
メソッドでポートを開く - データの送受信を行う
-
Close()
メソッドでポートを閉じる
例えば、以下のコードで通信ができる。
using System;
using System.IO.Ports;
class PingPort
{
public static void Main(string[] args)
{
string portName = args.Length > 0 ? args[0] : "COM1";
int baudRate = 115200;
Console.WriteLine("opening port \"" + portName + "\"");
SerialPort port = new SerialPort(portName, baudRate);
port.Open();
string messageToSend = "ping";
Console.WriteLine("sending \"" + messageToSend + "\"");
port.WriteLine(messageToSend);
string messageReceived = port.ReadLine();
Console.WriteLine("received \"" + messageReceived + "\"");
port.Close();
}
}
以下が実行例であり、LF で終わるテキストの送受信ができていることがわかる。
SerialPort が持つ名前こだわり
同じプログラムで、先ほど使えることを確認したポート CNCA0
での通信をしようとすると、エラーになってしまう。
YUKI.N>PingPort CNCA0
opening port "CNCA0"
ハンドルされていない例外: System.ArgumentException: 指定されたポート名の先頭が COM/com でないか、または有効なシリアル ポートではありません。
パラメーター名:portName
場所 System.IO.Ports.SerialStream..ctor(String portName, Int32 baudRate, Parity parity, Int32 dataBits, StopBits stopBits, Int32 readTimeout, Int32 writeTimeout, Handshake handshake, Boolean dtrEnable, Boolean rtsEnable, Boolean discardNull, Byte parityReplace)
場所 System.IO.Ports.SerialPort.Open()
場所 PingPort.Main(String[] args)
ポートは有効であるにもかかわらず、「ポート名の先頭が COM/com でない」というだけの理由で、合理性なくエラーになってしまうのである。
なお、「ポート名の先頭が COM
でないと例外を吐く」というのは、以下のように Open()
のドキュメントに明示されている。
ArgumentException
The port name does not begin with "COM".-or-
The file type of the port is not supported.
検証のため、名前が COM
で始まるポートを追加してみる。
すると、このポートでは普通に通信することができた。
ということは、use Ports class がオンになっていないと通信できないなどの技術的な理由ではなく、やはり本当に単にポートの名前が気に入らないというだけの理由で蹴っているようである。
SerialPort が持つ名前こだわりの害を軽減する
C# の SerialPort
では、合理性のない名前こだわりのため、有効なポートであっても、ポート名が COM
で始まらないというだけで開こうとすると例外が出てしまって開くことができない。
しかし、そのような名前が気に入らないポートであっても、有効なポートであるため、SerialPort.GetPortNames()
では普通に列挙対象に含まれる。
そのため、この列挙結果をそのまま選択肢としてユーザーに提示してしまうと、使えないポートが選択肢に含まれてしまうことがあり、不親切になってしまう。
(そもそも、本当に不親切なのは、有効なポートでも名前が気に入らないだけで例外を吐きやがる SerialPort
の方なのだが)
SerialPort
を使わず、Windows API を用いて通信することで、SerialPort
のこだわりに振り回されるのを回避できるかもしれない。
しかし、そうすると Windows に依存することになるし、DLL の読み込みの手間も生じる。
そこで、今回は列挙結果から COM
で始まらないポートを外すことで、SerialPort
で使えるポートのみを選択肢としてユーザーに提示できるようにする。
「列挙結果から COM
で始まらないポートを外す」処理は、以下の処理からなる。
- 文字列が
COM
で始まるかを判定する - 配列の要素のうち指定した条件を満たす要素のみを取り出す
前者は String.StartsWith
メソッドで、後者は Array.FindAll
メソッドで実現できる。
大文字の COM
だけでなく小文字の com
も通すため、StringComparison.CurrentCultureIgnoreCase
を指定する。
using System;
using System.IO.Ports;
class ListPorts2
{
public static void Main(String[] args)
{
string[] ports = SerialPort.GetPortNames();
string[] portsFiltered = Array.FindAll(
ports,
str => str.StartsWith("COM", StringComparison.CurrentCultureIgnoreCase)
);
Console.WriteLine(string.Join("\n", portsFiltered));
}
}
YUKI.N>ListPorts
CNCA0
CNCB0
COM5
COM6
COMFORT
TEST
YUKI.N>ListPorts2
COM5
COM6
COMFORT
COM
で始まるポート名のみを出力できた。
まとめ
C# でシリアルポート経由の通信を行う SerialPort
クラスには合理性のない名前こだわりがあり、有効なポートであってもポート名が COM
で始まっていないというだけで開けない仕様であることを確認した。
そして、このこだわりのせいで SerialPort
で開けないポートを選択肢から外すため、文字列の配列から COM
で始まる要素だけを抽出する方法を紹介した。