PowerShellでシリアルポートを使ってシリアルコンソールに接続するターミナルソフトの簡易版スクリプトを作ってみた

# 概要
シリアルポートは随分前からレガシーになっているが、未だにスイッチのコンソールとかモデム端末でATコマンドなんかを少しだけいじることがある。Windowsには昔はハイパーターミナルが付属していたが今は無いのでTeraTerm等が使われる。ターミナルソフトがインストールされていればいいのだが、ちょっとだけ使いたいときインストールするのも面倒な状況で簡易的にターミナルソフトの代替ができるPowerShellスクリプトを作ってみた。
USB RS232Cの場合当然ながらデバイスドライバーは別途インストールが必要。

---

# 環境

- Windows PowerShell 2.0、3.0、5.1で確認 
    - 注意: ISE環境ではコンソール(キーボード/画面)入出力処理に制限があるためエラーになり使用不可。PowerShellコンソール環境で動作する。
- VT100エミュレーションは5.1からサポートされたためそれ以前はエスケープシーケンスがうまく表示できない
- PowerShell coreでは.Netアセンブリがうまく追加できず動作しなかった

<!-- シリアルポートの アクセス権はどうなのか Windows10標準ユーザーでやってみたが問題なし-->

---

# スクリプト説明

Powershellのブログ記事に基づいて.NetのSerialportクラスを使ったスクリプトを作成。単純にWriteLine()やReadLine()メソッドを使うだけだと、いちいち入力したり表示するたびにメソッド呼出しの入力操作が必要になって面倒なので以下の処理とした。
・シリアル受信イベントをRegister-ObjectEventで登録してコンソールに自動的に表示する
・キーボード入力を読み取って送信するループを繰り返す
これらによりターミナルソフト風の動作が可能

もし自動化スクリプトを作成するのならWriteLine()やReadLine()で逐次処理させたほうが扱いやすい。あくまでユーザーが対話的にオペレーションする場合を想定。

[参照 PowerShell Team Blog](https://blogs.msdn.microsoft.com/powershell/2006/08/31/writing-and-reading-info-from-serial-ports/)
[参照 .Net System.IO.Portsクラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.ports?view=netframework-4.7.2)

---

## 接続開始時のスクリプト
  COMポートオープンエラーとかRS232Cのパリティエラーとか例外処理関係は簡便のため割愛
  ログを取りたい場合は、トランスクリプトを事前に開始しておく。(Start-Transcriptでデータが取れる)

```powershell:powershell
### 接続開始時のスクリプト ###

# 1. COMポートのインスタンス生成 (COM1、9600bps、パリティ無し) パリティある場合::Even、::Oddとする
$c = New-Object System.IO.Ports.SerialPort "COM1", 9600, ([System.IO.Ports.Parity]::None)

# 2. DTR、RTSを設定 (機器・ケーブル仕様に合わせる)
$c.DtrEnable = $true
$c.RtsEnable = $true

# 3. ハンドシェイク無し (必要なら::RequestToSendや::XOnXOffとする)
$c.Handshake=[System.IO.Ports.Handshake]::None

# 4. 改行文字をCR(0x0D)に設定 (機器仕様に合わせる)
#  ※NewLineプロパティはWriteLineやReadLineメソッドに適用されるため本方法では動作に影響しない
#  実際の変更方法の例は後述
$c.NewLine = "`r"

# 5. 文字コードをSJISに設定 (機器仕様に合わせる)
$c.Encoding=[System.Text.Encoding]::GetEncoding("Shift_JIS")

# 6. シリアル受信イベントを登録(受信したらコンソールに出力)
$d = Register-ObjectEvent -InputObject $c -EventName "DataReceived" `
    -Action {param([System.IO.Ports.SerialPort]$sender, [System.EventArgs]$e) `
        Write-Host -NoNewline $sender.ReadExisting()}

# 7. COMポートを開く
$c.Open()

# 8. キーボード入力をシリアルポートに送信する無限ループ (ReadKey($false)とすればローカルエコーになる)
#    これ以降、ターミナルソフトのようなキーボード入力と機器の出力表示になります (コピペデータも機器に送られるので注意)
#    終了時はctrl-cで抜ける
for(;;){if([Console]::KeyAvailable){$c.Write(([Console]::ReadKey($true)).KeyChar)}}


```
<!-- イベントでwrite-hostするとき、ReadKeyで待ってると、出力がリアルタイムに出ず待たされるようだ-->
<!-- 改行しない状態でCLI処理が不要な場合ならRead-Hostを使てもいい。Enterでまとめて送ることになる。 -->

---

## 実行例のスクリーンショット

### Yamahaルーターの例
![rtx810.png](https://qiita-image-store.s3.amazonaws.com/0/293449/468a5d78-fdc6-b239-1254-ed0e643f2999.png)


---

### LTEモデムの例
![atcmd2.PNG](https://qiita-image-store.s3.amazonaws.com/0/293449/eb679323-1765-d01a-9bde-42b7af856962.png)


---

### CentreCOM FS808TP V1の例(VT100)
※VT100エミュレーションはWMF 5.1からサポートされたのでそれ以前はうまく表示できない
![centreCOM_FS808TP.png](https://qiita-image-store.s3.amazonaws.com/0/293449/5f0925b5-c35e-633a-76cb-4113a60b8bba.png)
カラー表示がやや変だが一応動く

---

## 接続終了のスクリプト

本方法終了後、Powershellコンソールを続行するなら下記の手順を実行して後片付けする

```powershell:powershell
### 接続断時のスクリプト ###

# 9. COMポート閉じる
$c.Close()

# 10. イベント登録解除
Unregister-Event $d.Name

# 11. ジョブ削除
Remove-Job $d.id
```

Powershell自体を閉じてしまえばCOMポートとか各種リソースは解放されるのでそれでもいい。


---

## 改行コードの変更方法

### 受信する改行コードの変更
機器から受信する改行コードがCR+LFでない場合(CRのみやLFのみの場合)、6.の行内のWrite-HostのところにてCR+LFに置換する。(Windows PowershellコンソールはCR+LFで改行のため)

```powershell:powershell

#   機器から受信する改行がCRのみの場合、Write-HostするデータをReplaceで置換する 
$d = Register-ObjectEvent -InputObject $c -EventName "DataReceived" `
    -Action {param([System.IO.Ports.SerialPort]$sender, [System.EventArgs]$e) `
        Write-Host -NoNewline ($sender.ReadExisting()).Replace("`r","`r`n")}

#   LFのみの場合も同じくReplaceで置換する 
$d = Register-ObjectEvent -InputObject $c -EventName "DataReceived" `
    -Action {param([System.IO.Ports.SerialPort]$sender, [System.EventArgs]$e) `
        Write-Host -NoNewline ($sender.ReadExisting()).Replace("`n","`r`n")}

```
---

### 送信する改行コードの変更
機器へ送信する改行コードは8.の行内でEnterキーのReadKeyがCRになるので、下記のようにCR+LFまたはLFにする。

```powershell:powershell
# $c.Write()の引数を以下のように置換する例

#   EnterキーをCR+LFへ変更する場合
for(;;){if([Console]::KeyAvailable){$c.Write( `
    $(switch(([Console]::ReadKey($true)).KeyChar){{$_-eq[char]13}{"`r`n"}default{$_}}))}}

#   EnterキーをLFのみへ変更する場合
for(;;){if([Console]::KeyAvailable){$c.Write( `
    $(switch(([Console]::ReadKey($true)).KeyChar){{$_-eq[char]13}{"`n"}default{$_}}))}}

```

---

## その他
シリアル通信のエラー(パリティエラーとか)については、ErrorReceivedイベント処理を追加すれば対応できる。
シリアル通信の制御信号については、PinChangedイベント処理を追加すれば対応できる。