【C#,FTDI】COMポート選択時にデバイスの名前を併記して選びやすくする方法

# はじめに
まず，以下の画像に示すように，自作デスクトップアプリにおいて，COMポートを選択する際に，FTDIのシリアル変換デバイスに書き込んだプロダクト名を併記することを目指す．
![あああ - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340870/2085b158-c4d8-175a-979f-9bc0e5430681.png)

つまり，レジストリエディタを用い開発環境でだけ表示名変えるとかいうことではない．
また，Win32 APIで拾えるようなしょぼい名前（FIDIだとUSB Serial Converterとか）ではない．

WinUSBnetをNugetで使う？ ← どーやらドライバやINFから作る必要がある？のでナシ
UWPでWindows.USB.Deviceを使う手があるっぽい　← WinFormで作ってしまったのでアウト（しかしUSBViewを改造するよりスマートな気もする)

# 方針
0 . (Option) FT-Progでデバイス名をProductDescriptionに書き込む．
1. USBview.exeを改造してiProduct,Serialを引っ張り出す
1. Win32_PnPEntryでCOMポート,Serialを取得
1. Serialで紐付けして，表示する

## 0. FT-Progに書き込む
https://qiita.com/srs/items/85b58caff58128a4b486

![FTの画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340870/0fa8b692-2a5d-9014-5879-c69c03333794.png)

# 1.USBView.exeの改造
## 1.1準備
USBViewとは，接続されているUSBの結構な詳細を表示してくれるMicrosoft謹製ツール．Windows SDKに含まれている．ソースコードはこちら．[USBView](https://github.com/Microsoft/Windows-driver-samples/tree/main/usb/usbview)
こいつをビルドするために，VisualStudio, WIndowsSDK, WindowsDriverKit(WDK)をインストールする必要がある(おそらく)．

https://developer.microsoft.com/ja-jp/windows/downloads/windows-sdk/

https://learn.microsoft.com/ja-jp/windows-hardware/drivers/download-the-wdk

ちなみにWin8.1でもそれぞれ最新版インスイトールできた．
（当然開発側にのみインスイトール）
## 1.2 USBviewをとりあえずビルドできるようにする
githubからUSBviewをzipで落としてきて，・・・としたいが，USBviewだけの落とし方がわからんかったので，プロジェクト全体のレポジトリまるごと落とした．

https://github.com/microsoft/Windows-driver-samples/tree/main

で，USBviewの中の.slnファイル開いてVisualStudioを起動．

でとりあえずビルド実行すると，なんかが厳しすぎる，みたいなエラーが出たが，下記リンク参照で解決可能である．

https://learn.microsoft.com/ja-jp/visualstudio/msbuild/errors/msb8040?view=vs-2022

https://qiita.com/jazzsuna/items/acab232c197057c489d2

よく見たら，例のintelのMeltdownとSpectreの脆弱性の名を関しておるやん・・・

## 1.3 無理やりDescriptorを引っ張り出す

USBviewてソフト上ではUSB Descriptorが表示できる=外部に持ち出して利用できる，てわけだ．
まずデバッガで潜って，どこに行けば情報持ってこれるかを確認する
すると，`enum.c`の`EnumerateHubPorts`て関数内部，1380行目付近の`info`にアクセスすれば，以下のようにDescriptorが持ち出せることがわかる．

![debfig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340870/6779dbad-d483-664f-8d87-bdd8ca4e9c38.png)

**！！ここから力技 その1！！**
ので，この関数の終わりの方で，以下のようにtxtに書き出す！！！！

```csharp
if (info->StringDescs != 0x00) {
    if (info->StringDescs->Next != 0x00) {
        if (info->StringDescs->Next->Next != 0x00) {
            //出力用の配列
            char mc1[] = "ABCDEFGHIJKLMNOPQRSTUVRSTYZ";
            char cr[] = "\n"; 

            //device pathからvid,pid,serialを取得
            //ここもガード入れたほうが良さそう
            char* vid = info->UsbDeviceProperties->DeviceId;

            //Descriptorがワイド文字なので，扱いやすくcharにしとく
            size_t len;
            wcstombs_s(&len, mc1, sizeof(mc1), info->StringDescs->Next->Next->StringDescriptor[0].bString, _TRUNCATE);

            //デバッグ用に一応print出力
            OutputDebugString(mc1);
            OutputDebugString(cr);

            //txtに書き出す
            FILE* file;
            auto err = fopen_s(&file,"deviceInfo.txt", "a");
            if (err != 0) {
                OutputDebugString("FileOpenErr");
            }
            else {
                fprintf(file, vid); //device path
                fprintf(file, ",");
                fprintf(file, mc1);
                fprintf(file, cr);
                fclose(file);
            }
            
        }
    }
}
```
これはひどい！
StandardOutputにしてPsInfoで拾うパターンもあるな・・・とQiitaに書きながら思った．

結果
```
USB\VID_056E&PID_00E9\6&37XXXXYYYYXX,ELECOM MMO Mouse
前半にvid,pid,Serial含むdevicePath，後半にDescriptorに記載されたiProduct
```

## 1.4 USBviewを中断させる
ここまででとりあえず，Descriptorを出せるようになったが，このままだと，USB記述子を取りたいだけなのに，GUIのUSBVビュワーが起動してまう．よってこれも力づくで止める

まず，`uvcview.c`の270行目，`WinMain`関数でソフトが起動しかけ，
同ファイル320行目if文の評価部で叩かれる`CreateMainWindow(nCmdShow)`の中でさっき弄った関数が叩かれていそうである．よって，
`WinMain`関数開始直後に
```csharp
FILE* file;
fopen_s(&file, "deviceInfo.txt", "w");
fclose(file);
```
を仕込み，上書きWモードでfopenすることでtxtを初期化，
**！！ここから力技 その2！！**
`CreateMainWindow(nCmdShow)`が通ったすぐ次で
```csharp
if (1) {
    exit(EXIT_SUCCESS);
}
```
を入れて強制終了させる！！！

これで，PCに繋がってるUSBのDeviceIDとiProductのリストがtxtで取得できたことになる．
例
![txt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340870/f8e82541-6790-d703-655d-856ddf793295.png)


これでUSBView.exeの改造おしまい．Releaseでビルド（Spectre対処再び必須）して適当にbinフォルダからexeを回収してOK．
未署名なのでWindows SmartScreenに怒られるけど，一回実行すれば出ないのでOKということにする．

# 2.Win32_PnPEntryでvid取得

## 2.1 とりあえずデバイス名を出せるようにする
これはググればすぐ出てくる．
ありがてえ先駆者様様

https://www.paveway.info/entry/2023/01/01/csharp_getserialdevicenamelist

紹介されているコードの`GetSerialDeviceNameList()`をありがたく頂戴する
そして，COMポート情報を表示する`textBox2`とかを適当に用意して，以下のように使えるようにする．
```csharp
//COMの詳細を表示する
var portnamelists = GetSerialDeviceNameList();
textBox2.Text = "";
foreach (var line in portnamelists)
{
    textBox2.Text += line + Environment.NewLine;
}
```
このままだと，

![ないやつ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340870/39b02664-4bfd-e375-ea2d-22c9356b866c.png)


みたいに一体何のデバイスをつないでいるのかサッパリである．
ここで，さっき取ったvid,pid,serialから，横にiProductを表示させる！

## 2.2 WIn32_PnPからvid,pidを取得する
Win32_PnPはiProductは取れないが，vid,pid,serialは取れる．

https://learn.microsoft.com/ja-jp/windows/win32/cimwin32prov/win32-pnpentity

先駆者関数を少し変更する．あんまり引用しすぎると申し訳ないので，抜粋．
必要部分をコピーして上書きしていただきたい．
```csharp
foreach (var managementObject in managementObjectCollection)
{
    // デバイス名と同じようにDeviceIDも取得する．
    var nameValue = managementObject.GetPropertyValue("Name");
    var deviceID = managementObject.GetPropertyValue("DeviceID");

    //割愛，このままコピペしてもダメだぞ！

    //DevideIDも同じようにToStringする．
    var name = nameValue.ToString();
    var did = deviceID.ToString();
    
    if (SeralPortPattern.IsMatch(name))
    {
        var devname = "";

        //ここでシリアルポートもつデバイスの製品名を知りたい。
        //ので、VID,PID,Serialを送ってデバイス名を検索する。
        var vid = getVID(did);
        if (vid.Length> 4)
        {
            devname = getDeviceName(getVID(did), getPID(did), getSerialFromDeviceID(did));
        }

        // デバイス名リストに，持ってきたiProductも並べたる
        serailDeviceNameList.Add(name+" : "+devname);
    }
} 
```
# 3. Serialで紐付けして表示
## 3.1 検索関数を導入

2.2の変更コードを貼ると，そんな関数無いって怒られると思うので，以下の自作関数を適当なとこに配置する．
やってることは，先駆者関数でゲットしたVid,Pid,Serialをさっきのtxtから検索して横においてるだけ．

```csharp
//文字列からVIDをVID_XXXXの形で抜き出して返す。
private string getVID(string deviceid)
{
    if(deviceid.IndexOf("VID_") != -1)
    {
        //4 is [VID_] , 4 is [XXXX]
        return deviceid.Substring(deviceid.IndexOf("VID_"), 4 + 4);
    }
    return "";
}

//文字列からPIDをPID_XXXXの形で抜き出して返す。
private string getPID(string deviceid)
{
    if(deviceid.IndexOf("PID_") != -1)
    {
        //4 is [PID_] , 4 is [XXXX]
        return deviceid.Substring(deviceid.IndexOf("PID_"), 4 + 4);
    }
    return "";
}

//deviceIDの文字列からSerialNumberを8文字抜いて返す。
private string getSerialFromDeviceID(string deviceid)
{
    string res = "";
    if(deviceid.IndexOf("PID_") != -1)
    {
        try
        {
            //4 is [PID_] , 4 is [XXXX], 1 is [\]
            res = deviceid.Substring(deviceid.IndexOf("PID_") + 4 + 4 + 1, 8);
        }
        catch(Exception e) {
            Debug.WriteLine("getSerial-DevID-Err : " + e.ToString());
        }
    }
    return res;
}

//usbDevicePathからSerialNumber８文字を抜き出して返す。
private string getSerialFromTXT(string devPath)
{
    string res = "";
    var idx = devPath.LastIndexOf("\\");
    if(idx != -1)
    {
        try
        {
            // 1 is [\]
            res = devPath.Substring(idx + 1, 8);
        }
        catch(Exception e)
        {
            Debug.WriteLine("getSeria-TXT-lErr : " + e.ToString());
        }
    }
    return res;
}

//vid,pid,Serialを受け取って、deviceNameを返す。
private string getDeviceName(string target_vid,string target_pid,string target_serial)
{
    Debug.WriteLine("Target " + target_vid + "," + target_pid + "," + target_serial);

    using (StreamReader reader = new StreamReader(@"deviceInfo.txt"))
    {
        //テキストファイルから行ごとにデバイス情報を取得。
        while (!reader.EndOfStream)
        {
            string line = reader.ReadLine();
            string[] values = line.Split(',');
            if(values.Length == 2)
            {
                var vid = getVID(values[0]);
                var pid = getPID(values[0]);
                var serial = getSerialFromTXT(values[0]);
                Debug.WriteLine("Line " + vid + "," + pid + "," + serial);

                //一致したらデバイス名を取得し返す。
                if (vid.Equals(target_vid) && pid.Equals(target_pid))
                {
                    if (serial.Equals(target_serial))
                    {
                        return values[1]; // deviceName
                    }
                }
            }
            else
            {
                Debug.WriteLine("deviceInfo.txt is not 2 cell in Row");
            }
        }
    }
    return "";
}
```

## 3.2 完成
1.4で作ったexeを`bin\Debug\.net7.0`のように，実行exeと同じディレクトリに置けばOK
2.1で書いたように，改造先駆者関数`List GetSerialDeviceNameList()`のListをforeachでtextboxに放り込めば以下のようにいい感じにできる．

![あああ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340870/961a1188-6beb-eab1-5d07-df610f3b23b8.png)


しかし，ST-LinkだけはSerialが文字化けして表示されないが，まあnameの時点で出てるしいいか！と対応してない．

## 4.今後の予定
UWPでUSBviewを置き換える．



<!--
[Qiita](http://qiita.com)
-->