シリアルポート選択 chooserial コマンドを自作

# 1. シリアルポート

Arduino、マイコンボードやUSBシリアル変換器をUSBポートに差し込むと、シリアルポートにそれぞれ現れます。

```terminal
% alias lsu
lsu='ls /dev/cu.*'

% lsu
/dev/cu.Bluetooth-Incoming-Port	/dev/cu.usbserial-0001
/dev/cu.usbmodem1101		/dev/cu.usbserial-2120
/dev/cu.usbmodem13301		/dev/cu.wchusbserial2120
/dev/cu.usbmodemHIDEF1		/dev/cu.wlan-debug
```

一部ですが、↓ざっとこんな感じです。


- マイコン系

|シリアルポート|内容　|
|-|-|
|/dev/cu.usbmodemHIDEF1 | [自作プログラマブルテンキー](https://qiita.com/nak435/items/71e8c6ac306a661f8be8)（ATMega32U4 ネイティブUSB）常時接続|
|/dev/cu.usbmodemC7FD1ADFA7491  |   自作ボード（ESP32-S3 ネイティブUSB） [CircuitPython REPL](https://learn.adafruit.com/welcome-to-circuitpython/the-repl)|
|/dev/cu.usbmodem0000000000001| [RP2040 Dev Board](https://74th.booth.pm/items/4492441) (USB CDC)|
|/dev/cu.usbmodem13201 |  Raspberry Pi Pico [Micropython REPL](https://docs.micropython.org/en/latest/esp8266/tutorial/repl.html)|
|/dev/cu.usbmodem12401 |  Seeed Studio XIAO RP2040|
|/dev/cu.usbmodem21201 |  Adafruit Feather nRF52840|

- Arduino系

|シリアルポート|内容　|
|-|-|
|/dev/cu.usbmodem11101  | Arduino Uno R3|
|/dev/cu.usbmodem11201  | Arduino Uno R4 Minia|
|/dev/cu.usbserial-1140 | [Arduino Nano 互換機（Atmega328p、CH340C）](https://ae01.alicdn.com/kf/Hf4531ced4ea34a64a7b5ab0aa9697d8bu/Boot-Type-c-USB-3-0-nuino-USB-16MHz-mhz-atmega328p.jpg_640x640.jpg_.webp)|

- USBシリアル変換器

|シリアルポート|内容　|
|-|-|
|/dev/cu.wchusbserial53250003491 |[ESP32ダウンローダ](https://docs.m5stack.com/en/accessory/esp32_downloader_kit)（WCH CH9102F）|
|/dev/cu.usbmodemBC5C61321D8E2 |  [Wch-Link(旧式)]()（WCH CH549G）|
|/dev/cu.usbmodem8D568F0612342 |  [Wch-LinkE-R0-1v3](https://akizukidenshi.com/catalog/g/gM-18065/)（WCH CH32V305FBP6）|
|/dev/cu.usbmodem05581  | [USB Console Stub](https://shop.pimoroni.com/products/usb-console-stub)（HOLTEK HT43B534-2）|
|/dev/cu.usbserial-120 |  [USB-TTL変換モジュール](https://www.aitendo.com/product/20813)（WCH CH340C）|

_もちろんですが、接続するUSBポートを変えると、数字部分が変化します。_

`WindowsOS`の `COMxx` が、短くてうらやましい。。。

# 2. 複数あるシリアルポートを簡単に選びたい

`screen`コマンドや、`esptool`、`mpremote`、`ampy`などのコマンドで、接続しているマイコンのシリアルポートを指定する必要があるが、毎回`ls`コマンドを叩いてシリアルポートを確認して入力するのが面倒なので、一覧から選択できる`chooserial`コマンド（<ins>choo</ins>se <ins>se</ins>rial）を作成した。


```terminal
% chooserial
1: /dev/cu.usbmodem1101
2: /dev/cu.usbmodem13301
3: /dev/cu.usbserial-0001
4: /dev/cu.usbserial-2120
5: /dev/cu.wchusbserial2120
Choose device number (1-5);5＜enter＞
/dev/cu.wchusbserial2120        
```

プロンプトは、`stderr`に出力し、番号入力で選ばれたシリアルポートを`stdout`に（改行無しで）出力します。
選択可能なシリアルポートが1つしかない場合は、プロンプトせず、そのポートを返します。
また、`write`モードでダミー`open`することで、他で使用中のシリアルポートを選択肢から除外します。

```terminal:使用例
% esptool.py --chip auto --port `chooserial` flash_id
1: /dev/cu.usbmodem1101
2: /dev/cu.usbmodem13301
3: /dev/cu.usbserial-0001
4: /dev/cu.usbserial-2120
5: /dev/cu.wchusbserial2120
Choose device number (1-5);2
esptool.py v4.6.2
Serial port /dev/cu.usbmodem13301
Connecting...
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (revision v0.1)
Features: WiFi, BLE
Crystal is 40MHz
MAC: xx:xx:xx:xx:xx:xx
Uploading stub...
Running stub...
Stub running...
Manufacturer: c8
Device: 4018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
Hard resetting via RTS pin...
```

```terminal:不正入力時の再プロンプト
% screen `chooserial` 115200
1: /dev/cu.usbmodem1101
2: /dev/cu.usbmodem13301
3: /dev/cu.usbserial-0001
4: /dev/cu.usbserial-2120
5: /dev/cu.wchusbserial2120
Choose device number (1-5);6
Choose device number (1-5);x
Choose device number (1-5);?
1: /dev/cu.usbmodem1101
2: /dev/cu.usbmodem13301
3: /dev/cu.usbserial-0001
4: /dev/cu.usbserial-2120
5: /dev/cu.wchusbserial2120
Choose device number (1-5);2

ESP-NOW Recv Ready!

(Ctrl-A Ctrl-K)
[screen is terminating]
%
```

シェルの機能で`tab`キーによるパス名補完があるが、それでも`chooserial`コマンドに慣れると、圧倒的に楽です。


# 3. ソースコード

`C++`で作りました。

```c++:chooserial.cpp
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
vector<string> lsu;
void promptN() {
    fprintf(stderr, "Choose device number (1-%d);", lsu.size());
}
void prompt() {
    int n = 1;
    for (const auto& s: lsu) {
        cerr << n++ << ": " << s << endl;
    }
    promptN();
}
const int BUF = 256;
char buf[BUF];
int run(string cmd) {
    FILE *fp;
    fp = popen(cmd.c_str(), "r");
    if (!fp) {
        cerr << "popen error" << endl;
        return -1;
    }
    while (fgets(buf, BUF, fp)) {
        buf[strlen(buf) - 1] = '\0'; //remove last '\n'
        lsu.push_back(string(buf));
    }
    pclose(fp);

    for (auto it = lsu.begin(); it != lsu.end();) {
        //filter starts_with("cu.")
        if (it->substr(0, 3).compare("cu."))
            it = lsu.erase(it);
        else {
            *it = "/dev/" + *it;
            //dummy open 
            FILE *fp = fopen(it->c_str(), "w");
            if (fp) ++it, fclose(fp);
            else it = lsu.erase(it);
        }
    }
    //for (const auto& s: lsu) cout << s << endl; //for debug
    return lsu.size();
}
int strToi(const string s, int defaultValue = 0) {
    int result;
    try {
        result = stoi(s);
    } catch (invalid_argument& e) {
        return defaultValue;
    }
    return result;
}
int main(void) {
    switch (run("ls /dev")) {
    case -1: //fallthrough
    case 0: cout << "NO_DEVICES"; return -1;
    case 1: cout << lsu[0]; return 0;
    default: break;
    }

    prompt();
    int n = -1;
    do {
        if (fgets(buf, BUF, stdin)) {
            if (buf[0] == '?') { prompt(); continue; }
            if ((n = strToi(string(buf), -1)) >= 0) {
                if (n < 1 || lsu.size() < n) n = -1;
            }
        }
        if (n == -1) promptN();
    } while (n == -1);
    
    cout << lsu[n - 1];
    return 0;
}
```

```terminal:compile & linkage
% c++-13 chooserial.cpp -o chooserial
```

_コマンド名`chooserial`が長いと思う貴方は、どうぞ短い名前を付けてご利用ください。_




<hr>

関連リンク；

https://qiita.com/nak435/items/1c28778ffbb795a3fca2#おまけその2


以上