目的
シリアル通信は古くから存在しているため、それを実装するプログラムのサンプルも相応に古い。
10年以上前のコードが引用されていることもしょっちゅうだ。
でもいい加減、(比較的)新しい書き方をしたサンプルがあってもいいと思う。
ヒント集として、自分の断片的な知識をまとめてみた。
対象環境は製造業でよく遭遇する、Windows系+C#とする。
ヒント集
とにかくLINQを使おう
例えば入出力をログ出力したい時、チェックサムを計算するために全バイトにXORをとる時、文字列の16進数をバイト列に直したいとき。
各バイトに対して全く同じ処理を行う場合はLINQの使いどきだ。
以下イメージ。
public static class ByteArrayExtension
{
public static string ToHexFormattedString(this IEnumerable<byte> source)
=> string.Join(" ", source.Select(x => x.ToString("X2")));
public static byte XorAll(this IEnumerable<byte> source)
=> (byte)source.Aggregate(0, (nextItem, xorResult) => nextItem ^ xorResult);
}
「1バイトをToString("X2")
して結果をスペースで区切る」とか、「とにかく全部でXORを取る」とかの「何をするか」のみが書かれていて、
ループ用の変数などの処理には直接は関わらないものが上記コードには含まれないため、意図がわかりやすいはずだ。
このクラスをインポートした上で、例えば(new byte{1,2,3}).ToHexFormattedString()
のように記述すれば、
01 02 03
の文字列が帰ってくる。
ついでだから拡張メソッドを利用して、何をソースにしているかをわかりやすくしておいた。
また、Listやbyte[]を引数に指定しないのは別にこの時点では型を気にする必要がないことと、yieldを使いたいからである。
バイト列の加工にはyieldを使う
特にデータを送信する場合、例えば「STX(0x02)で始まりETX(0x03)で終わるデータを送信データとしたいため、
ETXが送信データに入ってしまった場合はそのバイトだけ反転してほしい」といった特殊処理が必要になることがたまにある。
その場合、旧来のプログラムの書き方の影響が強い場合、データを送る前に配列をループして確認することが多い。
だがちょっとまってほしい。そのルールは「送信用のバイト列の作成」に合流させられないだろうか。
例えばこのように。
private static IEnumerable<byte> CreateSendFrame(IEnumerable<byte> rawDataBytes)
{
yield return 0x02;
byte checkSum = 0x02;
foreach (var item in rawDataBytes)
{
byte next = item != 0x03 ? item : (byte)(item ^ 0xFF);
checkSum ^= next;
yield return next;
}
yield return checkSum;
yield return 0x03;
}
配列に対しての操作で対応する場合、その配列自体が読みづらさが増す原因となりやすい。
少しでも考えることを減らそうとしたらこのような関数にいきついた。
また、このような配列にしておけばエスケープバイトの追加などのバイト列の増減への対応も楽になるはずだ。
文字列を扱うときもバイト列で受信する選択肢を考える
例えば上記のような特殊な処理が文字列内にも入っている場合や、
メモリのデータを直接読み取ることによって発生する、2バイト単位でバイトが入れ替わっている文字列データなど、
受信したあとに一工夫が必要な文字列をしていることがよくある。
それをSeralPort.ReadLineメソッドなどで読み込んだ場合、エンコーディングできない文字列はEncoderFallBackの設定により?に置換されてしまう。
このフォールバックの発生は抑えられないため、エンコーディングに適さないバイト列は文字列型にはどうやっても押し込めることができないことに気をつけよう。
自分の周囲ではこれに引っかかっている人が意外に多かった。
(?にたいして変換しようとして失敗している人をよく見かけた)