【C#】com0comを使ったシリアル通信テスト

# この記事では

C#のアプリで、シリアル通信テストを行うための方法を書き残す

## 使用したツールの情報

- com0com 3.0.0.0
- TeraTerm 4.105
- .NET Core 3.1
    - System.IO.Ports 5.0.1


# com0com の設定

PC単体でCOMポートの接続テストをするために、com0comをインストールする

## インストールからデバイスドライバの更新まで

「com0com」でGoogle検索(.jp)すると、この記事が1番目にヒットする
記事の説明通りに進めれば、問題なくインストールできる

https://qiita.com/yaju/items/e5818c99857883a59033

### インストール時のWindows10バージョン

Windows10 21H1 にインストールした

![img3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/4a56c14d-68f2-d637-3fc8-963960e03354.png)

## com0com をセットアップする

ツールの場所: `"C:\Program Files (x86)\com0com\setupg.exe"`

今回は` {COM20, COM21}` と `{COM22, COM23}` の2セットペアを生成した

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/2f0832c9-a513-4023-281c-cb68c8223244.png)

## デバイスマネージャーで確認する

ドライバの更新が正常ではない場合、黄色三角のエラーマークが表示される

解消するためには、`Windows Update -> "オプション更新プログラム"` から所定のプログラムを更新する
（ドライバを更新する方法について、先述の記事に記載あり）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/0f3b08dd-62e2-a054-a3f6-8e0c0c70bb23.png)

# TeraTermの設定

`COM21`と`COM23`をコンソールで監視する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/de4a589a-5ef0-969c-2a1f-d2487bedac6b.png)

シリアルポートの設定はデフォルトのままとした

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/ff25de45-c3b1-6b6e-7da1-92303cc24952.png)


# C#アプリからシリアル通信コマンドを送信する

コードの詳細は[記事の最下部の付録に掲載](#付録)する

ウィンドウには`Send`ボタンのみが配置される
ボタンを押すと、`COM20`->`COM21`へ**数字18桁**、`COM22->COM23`へ**英字18桁**がそれぞれ送信される

## TeraTermでコマンドを受け取る

TeraTermを`COM21`, `COM23`に接続する
アプリの`Send`ボタンを押すたびに、受け取ったコマンドがそれぞれのコンソールに表示される

- `COM21`は`COM20`から受け取った数字を表示
- `COM23`は`COM22`から受け取った英字を表示

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/6f8d0b82-23d8-7c34-7c6c-537bab90c17a.gif)

## MySerialPortクラスでコマンドを受け取る

送信につかった`MySerialPort`クラス同様に
受信側も`MySerialPort`を使う

```cs:全体のコードは付録に記載
    public MainWindow()
    {
        InitializeComponent();

        _com20 = new MySerialPort("COM20");
        _com21 = new MySerialPort("COM21"); // <-- TeraTermを使うときはコメントアウト
        _com22 = new MySerialPort("COM22");
        _com23 = new MySerialPort("COM23"); //  <-- TeraTermを使うときはコメントアウト
    }
```

TeraTermのときと違い、受け取ったByte配列は16進数で表示される

例）　"1"は16進数`31`、 "A"は16進数`41`

![Animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/d0b8c9b7-befc-17eb-62b4-f755211b86c6.gif)

# おわりに

参考にさせていただいた記事にもある通り

> 「com0com」はとても便利ですが、所詮は仮想ですので最終的な確認は本物のシリアルポートがあるPCでやりましょう。

# 付録

検証に使ったコードを掲載する

## シリアルポート操作用モデル

コマンドを送信する`Send`メソッドだけを公開している

データを受け取った際にイベントが発火し、`Receive`メソッドにより受け取ったデータを`byte[]`に格納する

```cs:MySerialPort.cs
using System;
using System.Diagnostics;
using System.IO.Ports;

public class MySerialPort
{
    private readonly SerialPort _sp;

    public MySerialPort(string portName)
    {
        _sp = new SerialPort()
        {
            PortName = portName,
            BaudRate = 9600,
            DataBits = 8,
            Parity = Parity.None,
            StopBits = StopBits.One,
            Handshake = Handshake.None
        };

        _sp.Open();
        _sp.DataReceived += OnDataReceived;
    }

    private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        Debug.Write($"{_sp.PortName} Data Received");

        byte[] receivedBytes= Receive();
        int receivedBytesLength = Buffer.ByteLength(receivedBytes);
    
        for(int i = 0; i < receivedBytesLength; i++)
        {
            Debug.Write($"{receivedBytes[i]:X},");
        }
        Debug.Write("\n");
    }

    public void Send(string message = "DefaultMessage")
    {
        _sp.Write(message);
        Debug.WriteLine($"{_sp.PortName} send {message}");
    }

    private byte[] Receive()
    {
        byte[] buf = new byte[128];
        int totalReadBytes = 0;

        while(true)
        {
            int bytesToRead = _sp.BytesToRead;

            if (bytesToRead == 0)
                break;

            int readBytes = _sp.Read(buf, totalReadBytes, bytesToRead);

            totalReadBytes += readBytes;
            Debug.WriteLine($"{_sp.PortName} toRead: {bytesToRead}, readBytes:{readBytes}, total: {totalReadBytes}");
        }

        byte[] receivedBytes = new byte[totalReadBytes];
        Array.Copy(buf, receivedBytes, totalReadBytes);

        return receivedBytes;
    }
}
```

https://docs.microsoft.com/ja-jp/dotnet/api/system.io.ports.serialport

## 画面のコードビハインド

`MySerialPort`クラスのインスタンスを4つ生成する
com0comで設定した4つのポート名をコンストラクタの引数に与えている


ボタンにはイベントハンドラ`Send(object sender, RoutedEventArgs e)`が紐づいており
COM20からは数字18桁、COM22からは英字18桁が送信される

com0comで20と21はペアになっているので、20から送信した内容は21のポートで受信される
（COM22->COM23も同様）

```cs:MainWindow.xaml.cs
using System.Threading.Tasks;
using System.Windows;

public partial class MainWindow : Window
{
    private readonly MySerialPort _com20;
    private readonly MySerialPort _com21;
    private readonly MySerialPort _com22;
    private readonly MySerialPort _com23;

    public MainWindow()
    {
        InitializeComponent();

        _com20 = new MySerialPort("COM20");
        //_com21 = new MySerialPort("COM21"); // <-- TeraTermを使うときはコメントアウト
        _com22 = new MySerialPort("COM22");
        //_com23 = new MySerialPort("COM23"); //  <-- TeraTermを使うときはコメントアウト
    }

    private void Send(object sender, RoutedEventArgs e)
    {
        _ = Task.Run(() => _com20.Send("123456789123456789"));
        _ = Task.Run(() => _com22.Send("ABCDEFGHIABCDEFGHI"));
    }
}
```