キーボードエミュレータを使った自動キーボード入力でファイルを他端末にコピーする

PC から別の PC に、キーボードエミュレータを使ってファイルをコピーしてみます。ウィルス感染対策などで USB メモリや LAN の使用が禁止されている場合に役立つ、かもしれません。

動作確認環境

  • Windows 11 Home 22H2

みんラボのキーボード/マウスエミュレータ

自動キーボード入力を行うため、みんなのラボの「キーボード/マウス エミュレータ(USB 接続版)」を利用します [1]。私は秋葉原の Shigezone にて 1650 円で購入しました。

青い USB メモリのようなものが本体です。USB A-micro B ケーブルも付いていましたが、Shigezone による数量限定のオマケのようです。

接続は次のようになります。

右側の PC から仮想 COM ポートに対してデータを出力すると、左側の PC に人間がタイプしたかのようにキーボード入力されます(マウス入力もできます)。

仮想 COM ポート側 PC のデバイスマネージャを見ると、「USB-SERIAL CH340」のドライバーが読み込まれ COM4 が割り当てられていました。

装置の VID 0x1A86 = 6790 を usb.org で調べると、「Nanjing Qinheng Microelectronics Co., Ltd.」とありました。

中国語で書くと「南京沁恒微电子股份有限公司」。百度地图の全景(Google ストリートビューに相当)で本社を見てみると、なかなか立派なビルです [2]。

もう一方のキーボード入力側 PC には、マイクロソフトのキーボードドライバーが読み込まれていました。装置の VID は同じく 0x1A86 です。

ファイルのコピー手順

今回の目的はファイルのコピーですが、バイナリデータや漢字をキーボード入力で転送するのは困難なので、base64 を経由してコピーすることにします。

コピー元の PC でファイルを base64 文字列に変換しておき、本装置を使ってキーボード入力で文字列を転送し、コピー先 PC で元のファイルに戻すというわけです。
base64 のエンコードとデコードには、Windows 付属の certutil コマンドが使えます。

ファイル転送プログラムの作成

ファイルをコピーするには、コピー元の PC で「ファイルを読み取って、キーボード入力のデータに変換し、仮想 COM ポートに出力するプログラム」を動かす必要があります。
Web からダウンロードできる本装置の解説書に、コマンドの送り方や Python のサンプルプログラムが載っているので [3]、これを参考にプログラムを書きます。

Python が入っていない環境でも使えるよう PowerShell で書いたプログラムが以下です。信頼性は十分ではないと思いますので、業務に使う場合はご注意ください。また、仮想 COM ポートの名前($COM_PORT_NAME)と読み取るファイルの名前($BASE64_FILENAME)は、状況に合わせて変更してください。

# TypeCopy.ps1
# みんなのラボ キーボード/マウスエミュレータ《USB接続版》を使って
# Windows 端末から別の Windows 端末に base64 文字列をキーボード入力する
# Windows PowerShell スクリプト。

# デバイスマネージャーを参照して、
# キーボードマウスエミュレーターを接続している COM ポートを指定のこと。
$COM_PORT_NAME = 'COM4'

# certutil -f -encode <入力ファイル名> <出力ファイル名>
# で生成した base64 の出力ファイル名を指定のこと。
# 任意の文字が送れるわけではないので(漢字などは送れないので)注意。
$BASE64_FILENAME = 'c:\tmp\a.base64'

# comport オブジェクトの定義
$g_comport = New-Object System.IO.Ports.SerialPort $COM_PORT_NAME, 9600

# 各文字のキーコードの定義
# (Modifier << 8) | Key Code
# 0xFFFF はエラー
[uint16[]] $KEY_CODE_LIST = @(
#   <NUL>  <SOH>   <STX>   <ETX>   <EOT>   <ENQ>   <ACK>   <BEL>
    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
#   <BS>   <TAB>   <LF>    <VT>    <FF>    <CR>    <SO>    <SI>
    0xFFFF, 0x002B, 0x0028, 0xFFFF, 0xFFFF, 0x0028, 0xFFFF, 0xFFFF,
#   <DLE>  <DC1>   <DC2>   <DC3>   <DC4>   <NAK>   <SYN>   <ETB> 
    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
#   <CAN>  <EM>    <SUB>   <ESC>   <FS>    <GS>    <RS>    <US> 
    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
#   <SP>   !       "       #       $       %       &       '
    0x002C, 0x021E, 0x021F, 0x0220, 0x0221, 0x0222, 0x0223, 0x0224,
#   (       )       *       +       ,       -       .       /
    0x0225, 0x0226, 0x0234, 0x0233, 0x0036, 0x002D, 0x0037, 0x0038,
#   0       1       2       3       4       5       6       7
    0x0027, 0x001E, 0x001F, 0x0020, 0x0021, 0x0022, 0x0023, 0x0024,
#   8       9       :       ;       <       =       >       ?
    0x0025, 0x0026, 0x0034, 0x0033, 0x0236, 0x022D, 0x0237, 0x0238,
#   @       A       B       C       D       E       F       G
    0x002F, 0x0204, 0x0205, 0x0206, 0x0207, 0x0208, 0x0209, 0x020A,
#   H       I       J       K       L       M       N       O
    0x020B, 0x020C, 0x020D, 0x020E, 0x020F, 0x0210, 0x0211, 0x0212,
#   P       Q       R       S       T       U       V       W
    0x0213, 0x0214, 0x0215, 0x0216, 0x0217, 0x0218, 0x0219, 0x021A,
#   X       Y       Z       [       \       ]       ^       _
    0x021B, 0x021C, 0x021D, 0x0030, 0x0089, 0x0031, 0x002E, 0x0287,
#   `       a       b       c       d       e       f       g
    0x022F, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A,
#   h       i       j       k       l       m       n       o
    0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0010, 0x0011, 0x0012,
#   p       q       r       s       t       u       v       w
    0x0013, 0x0014, 0x0015, 0x0016, 0x0017, 0x0018, 0x0019, 0x001A,
#   x       y       z       {       |       }       ~       <DEL>
    0x001B, 0x001C, 0x001D, 0x0230, 0x0289, 0x0231, 0x022E, 0xFFFF
)

# コマンドパケットの定義
# HEAD#1, HEAD#2, ADDR, CMD, DATALEN, DATA..., SUM
[byte[]] $CMD_RESET   = @(0x57, 0xAB, 0x00, 0x0F, 0x00, 0x11)
[byte[]] $CMD_RELEASE = @(0x57, 0xAB, 0x00, 0x02, 0x08, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C)
[byte[]] $CMD_HANKAKU = @(0x57, 0xAB, 0x00, 0x02, 0x08, 
    0x02, 0x00, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99)

function SetChecksum([ref] [byte[]] $buf)
{
    # 配列の末尾にチェックサムをセットする。

    $sum = 0
    for ($i = 0; $i -lt $buf.Value.length - 1; $i++)
    {
        $sum = ($sum + $buf.Value[$i]) -band 0xFF
    }
    $buf.Value[$buf.Value.length - 1] = $sum
}

function SendDataToEmu([byte[]] $cmd)
{
    # キーボードエミュレーターに指定のデータを送る。

    # コマンドパケット送信
    $g_comport.Write($cmd, 0, $cmd.length)

    # コマンド送信時のウェイト(必要に応じて有効化・時間調整)
    # Start-Sleep -Milliseconds 10

    # アンサーパケットの受信待ち
    # HEAD#1, HEAD#2, ADDR, CMD, DATALEN, DATA..., SUM
    [byte[]] $ans = New-Object byte[] 16

    # アンサーパケットを確認すると処理が遅くなるので、
    # 速度優先であれば次の 2 行を有効にして読み捨ててもよい。
    # $bytes = $g_comport.Read($ans, 0, $ans.Length)
    # return

    for ($i = 0; $i -lt 5; $i++)
    {
        $read = $g_comport.ReadByte();
        $ans[$i] = $read;
    }
    if ($ans[4] -gt 8)
    {
        throw "アンサーパケットが長すぎる, cmd=$cmd, ans=$ans"
    }
    for ($i = 0; $i -lt $ans[4] + 1; $i++)
    {
        $read = $g_comport.ReadByte();
        $ans[5 + $i] = $read;
    }

    # 正常時: アンサーパケットの CMD = コマンドパケットの CMD | 0x80
    # 異常時: アンサーパケットの CMD = コマンドパケットの CMD | 0xC0
    # 仕様書には上記のようにあるが、なぜか正常時に
    # (コマンドパケットの CMD | 0x80) - 1 がセットされることがある。
    # 0xC0 ビットが立っていたら異常、と判断することにする。
    if (($ans[3] -band 0xC0) -eq 0xC0)
    {
        throw "データ送信失敗, cmd=$cmd, ans=$ans"
    }
}

# 開始時のウェイト(必要に応じて有効化・時間調整)
Start-Sleep -Milliseconds 3000

try
{
    # COM ポートのオープン
    $g_comport.Open()

    # リセットコマンド発行
    SendDataToEmu $CMD_RESET

    # IME を OFF にする
    SendDataToEmu($CMD_HANKAKU)
    SendDataToEmu($CMD_RELEASE)
    SendDataToEmu($CMD_HANKAKU)
    SendDataToEmu($CMD_RELEASE)

    try
    {
        # ファイルを開く
        $stream = New-Object System.IO.StreamReader($BASE64_FILENAME, 
            [Text.Encoding]::GetEncoding('Shift_JIS'))

        # 各行ごとのループ
        while (($line = $stream.ReadLine()) -ne $null) 
        {
            # Write-Host $line
            # 各文字ごとのループ
            for ($i = 0; $i -le $line.length; $i++)
            {
                # 1 文字取得
                $char = $line[$i]
                $ascii = [byte]$char

                # 行末だったら改行文字に読み替える
                if ($ascii -eq 0x00)
                {
                    $ascii = 0x0D
                }

                # 当該文字のキーコードを取得
                $keycode = $KEY_CODE_LIST[$ascii]
                if ($keycode -eq 0xFFFF)
                {
                    throw "この文字は送信できません, ascii=$ascii"
                }

                # キー押下
                [byte[]] $cmdBuf = @(0x57, 0xAB, 0x00, 0x02, 0x08, 
                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
                $cmdBuf[5] = ($keycode -shr 8) -band 0xFF # modifier
                $cmdBuf[7] = ($keycode -band 0xFF) # key
                SetChecksum ([ref] $cmdBuf) # checksum
                SendDataToEmu $cmdBuf

                # キー解放
                SendDataToEmu $CMD_RELEASE
            }
        }

        # リセットコマンド発行
        SendDataToEmu $CMD_RESET
    }
    finally
    {
        # ファイルのクローズ
        if ($stream -ne $null)
        {
            $stream.Close()
            $stream.Dispose()
        }
    }
}
finally
{
    Start-Sleep -Milliseconds 100 # 念のため

    # COM ポートのクローズ
    # 未オープン状態でクローズ指示しても例外発生しない
    $g_comport.Close()
    $g_comport.Dispose()
}

画像ファイルをコピー

それでは実際に自動キーボード入力によるファイルのコピーをしてみましょう。

コピー元のファイルとして、彦根城で撮影したひこにゃんの写真「hikonyan.jpg」を用意しました。ファイルサイズは 25KB です。

certutil コマンドを使って、base64 のテキストファイルに変換します。ファイルサイズは 34KB に増えました。

  C:\tmp>certutil -encode -f hikonyan.jpg a.base64
  入力長 = 25333
  出力長 = 34892
  CertUtil: -encode コマンドは正常に完了しました。

コピー先の PC でメモ帳を起動し、フォーカスを当てておきます。

コピー元の PC に戻って、さきほど作ったファイル転送プログラム「TypeCopy.ps1」を実行します。

C:\tmp>powershell .\TypeCopy.ps1

コピーが始まりました。base64 文字列がタイプされていきます。

約 30 分後 (!)、コピーが終わりました。a.txt として保存します。

base64 をデコードし、もとのバイナリファイルに戻します。

  C:\tmp>certutil -f -decode a.txt a.jpg
  入力長 = 34892
  出力長 = 25333
  CertUtil: -decode コマンドは正常に完了しました。

無事、ひこにゃんの写真が転送されました。

おわりに

自動キーボード入力でファイルをコピーすることができました。
この仕組みを使って PowerShell のスクリプトをコピー先に送り、装置を逆に接続すると、逆方向のファイルコピーもできます。

コピーに時間がかかるのが難点であり、現状、1KB の送信に約 1 分もかかってしまいますが、チップの設定により通信速度が変えられるようなのでうまくすればもう少し高速化できるかもしれません。

参考文献

[1] みんなのラボ, キーボード/マウスエミュレータ,
http://www.minnanolab.net/product/pro_keyboardmouse/pro_keyboardmouse.html

[2] 百度地图,
https://map.baidu.com/@13220753.34,3737555.15,21z,87t,-0.56h#panoid=09002500121709121317369586L&panotype=street&heading=327.64&pitch=13.38&l=21&tn=B_NORMAL_MAP&sc=0&newmap=1&shareurl=1&pid=09002500121709121317369586L

[3] ichi’s workspace, キーボード/マウス エミュレータ,
https://sites.google.com/site/ichiworkspace/ホーム/みんなのラボ/キーボードマウスエミュレータ