6キー片手キーボードをカスタマイズする（調査編）

# はじめに

この手のデバイスは自作しているので[^1] [^2]、改めてこの製品が欲しいとは思いませんでしたが、たまたま[Amazon](https://www.amazon.co.jp/dp/B09Y5LCZRD/)で安く売られていて、レビューにある「**怪しい設定ソフト**」に興味を持ち、Mac用の設定ソフト[^7]でも自作しようかと考えて購入した次第。
購入時の価格；税込¥3,481.-送料無料

<img width="150" alt="sikaicase6key.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/531a2daf-842f-74a7-aece-b11465d2b40b.jpeg" />
<img width="300" alt="amazon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/0f208101-991d-690d-1a60-acd7e72618b2.png" />


### ・ 前置き


他者のレビューやAmazonの商品説明によると「設定ソフト」は、Googleドライブからダウンロードすることになっているが、届いた商品に同梱されていた「ユーザーマニュアル」には、QRコードがあり、『スキャンして設定ソフトをダウンロードする』と記載されていた。実際にスキャンすると[説明ページ（設定ソフトのダウンロードおよび使い方）](https://sikaicase.com/blogs/support/setting-for-software)が用意されていた。[日本語ページはこちら](https://sikaicase.com/blogs/news/sikai-case-mini-keyboard-software-setting-japanese)



<img width="300" alt="sikicase_site.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/303b4d9e-5401-c652-de25-b243cbcfc887.png" />


しかし、Edgeでダウンロードすると、セキュリティではじかれ「未確認.download」となるので、**自己責任**で元々のファイル名にリネーム。
また、ダウンロードファイルは拡張子が`.rar`のため、Windows標準機能では解凍できない。




# 分解

4本のネジを外し、キーキャップ・キースイッチ・つまみを抜くと、基板が取り出せる。

|||
|-|-|
|おもて|![IMG_1971.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/14970c84-7ac5-8651-14dc-e6122e66a1da.png)<br>Layer切替プッシュスイッチ、Bluetoothオン/オフスライドスイッチ、ロータリーエンコーダ、RGBLEDx10|
|うら|![IMG_1972.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/e65c7e12-12e0-5674-dab7-e441eeb5c777.png)<br>USB-C2.0ソケット、キーソケットx6、リチウムイオン電池、電子部品、基板右上にBluetoothのアンテナパターン|

元々、赤軸のキースイッチが付いていたが、ソケット式のため自分で好みのキースイッチに交換も可能。


<img width="146" alt="マイコン.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/ce10a4e9-44c8-ada3-3ef3-ecec9d2a7fe7.png" /> 
<img width="300" alt="ch573.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/d56b7873-db56-85f6-1dff-2643b4811676.png" />



マイコンと思われるICは、表面が削られており型番は不明であるが、『32ピン、4ミリ四方、USB、Bluetooth』の情報検索から、[**CH573X**](http://www.wch-ic.com/products/CH573.html)であると思われる。このマイコンは、クリスタル（水晶発振器）こそ外付けだが、4ミリ四方の1チップに、USB・BLE・PWN・UART・ADC・GPIOx22本を有し、5V→3.3Vレギュレータも内蔵しており、USB電源から直接3.3V MCUを駆動できる優れものである。詳しくは「[データシート（CH573DS1.PDF）](http://www.wch-ic.com/downloads/CH573DS1_PDF.html)」参照。

<img width="200" alt="IMG_1974.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/97271193-d4b3-714c-b0b8-0efcaa37cfb4.png" />

3.7V 200mAh仕様のリチウムイオンポリマー電池は、USBケーブルでPCに接続すると充電されるようだ。Bluetooth接続時はワイヤレスで使用できるが、バッテリ駆動時はLEDが一切点灯しない仕様。（モバイルバッテリー接続時はLEDは点灯する）

<img width="100" alt="crystal.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/34c3241d-c6b1-9a61-9523-80430df6ede1.png" />

<img width="114" alt="transistor.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/59ce35c5-2ff7-39b1-ced5-67888fa8dd11.png" />

抵抗とコンデンサを除くと、残りの電子部品は、クリスタルとトランジスタが2個だけ。

トランジスタは表面の文字から、Pch-MOSFET（A19T）とNPN型トランジスタ(1AM)で、使われてないスルーホールがすぐ横に見えるので、外部電源か何かの拡張用か？


<br>

# カスタマイズ方針

購入当初は、キースイッチ、ロータリーエンコーダとRGBLEDの各信号が接続されているマイコンのGPIO端子を調べ、マイコンのプログラムを直接書き換えてカスタマイズする方法を考えていた。しかし、分解してみると自分が扱ったことが無いマイコンであり、かつ、このマイコン用の開発環境を準備することが簡単ではないため、方針を変更する。

設定ソフトがこのデバイスとやりとりしている内容を解析し、このデバイスの設定インタフェースを模すことで、Macから設定できるアプリを作る方針とする。

メーカーが提供している設定ソフトはWindowsでしか動かないため、WindowsでUSBの通信データをキャプチャするソフトを使い解析する。
<br><br>

_2024.2.22追記_
_先日日本語サイトを見てみたら、Mac用設定アプリが追加されていた。_


# 設定インタフェースの確認

### 準備

1. USB通信をキャプチャするため、Windows10に[`Wireshark`](https://www.wireshark.org/download.html)と[`USBPcap`](https://desowin.org/usbpcap/)をインストールする。**Wireshark**を管理者権限で起動しないと、USBがインタフェースに現れません（インストールや使い方の説明は、他の記事に譲る）

2. PC環境に依存すると思われるが、自分のPCは`USBPcap3`が当デバイスの接続先（これを探すのに苦労した）

3. 一瞬にして何十万ものキャプチャデータが出てくるため、フィルタに`usb.bInterfaceClass == 0x03`を指定する。`0x03`はHIDデバイスを示す
 

## 解析結果

キャプチャしたHIDレポートディスクリプタを解析すると、1つのキーの設定につき、`03fd`から始まる65バイトの固定長データが2組、デバイスに送信されることが分かった。

下記は、Layer3のKey2に2つのキーコードを割り当てた例である。
![cap1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/29d7c0fe-8152-b48a-cf36-a78ced3f77b7.png)

**1stブロック**
- 1,2バイト目：03fd固定
- 3バイト目：Key番号（1〜6、0x10〜0x12）（例では2）
- 4バイト目：Layer番号（1〜3）（例では3）
- 5バイト目：データ種別（例では0x01）
- 11バイト目：後ろにつづくデータの個数（0〜）（例では2）
- 12バイト目以降：2バイトで1組のキーコード情報<br>上位バイト：キー モディファイア マスク（後述）<br>下位バイト：キーコード（後述）

**2ndブロック**
- 1,2バイト目：03fd固定
- 3,4バイト目：feff固定

## 1) Key番号

当デバイスには、6個のキーと、1個のノブ（ロータリエンコーダ）があり、Key番号は以下の通り。

|キー|Key番号||ノブ|Key番号|
|-|-|-|-|-|
|キー1|0x01||左回転|0x10|
|キー2|0x02||押し込み|0x11|
|〜|〜||右回転|0x12|
|キー6|0x06||||





## 2) キーコードおよびキー モディファイア マスク

- キーコード（スキャンコード）
A:0x4, B:0x5,・・・のコードで『[HID Usage Tables 1.4 - §10 Keyboard/Keypad Page (0x07)](https://www.usb.org/sites/default/files/hut1_4.pdf#page=90)』に`Usage ID`として定義されている。

- キー モディファイア マスク
複数のキーで修飾する場合は、下記マスク値の論理和を指定する。

|キー モディファイア|マスク値|
|:-|-:|
|left-Control|0x01|
|left-Shift|0x02|
|left-Alt/option|0x04|
|left-Win/command|0x08|
|right-Control|0x10|
|right-Shift|0x20|
|right-Alt/option|0x40|
|right-Win/command|0x80|

例　Alt+Ctrl+Del：054c（モディファイアマスク：0x05、キーコード：0x4c）

:::note info
ここで指定するキーコードは、OSに設定しているキーボード種別と密接に関係する。
例えば、日本語キーボードは、数字`2`キーのシフト時は`"`（ダブルクォーテーション）だが、英語キーボードだと数字`2`キーのシフト時は`@`（アットマーク）だ。
このように、日本語と英語キーボードでは、記号文字のキー配列が異なるため、どのOSでもPCに接続しているキーボード種別を設定するようになっている。[^3] [^4]

この**設定アプリ**は**英語キーボード**基準で作成されているため、OSに日本語キーボードが設定されていると 期待通りの文字がタイプされない現象が発生する。具体的には、`@`をタイプしたいため、設定ソフトで`Shift+`と`2@`を割り当てたとする。しかし。前述の理由により`"`がタイプされる。
対応策としては、『_打ちたい文字がある日本語キーボードのキートップの位置にあたる英語キーボードのキートップの文字_』を指定する必要がある。

Macで設定アプリを作成する場合は、こういった面倒な変換は不要な仕様にするつもりである。[^5]
:::


## 3) 1つのキーに何個のキーコードを指定できるか？

例えば、あるキーを押した時に、"Hello World!"とタイプしたい場合は、
`Shift+H E L L O space Shift+W O R L D Shift+1` の12個のデータを順にセットする。
データ構造上は、最大27個のデータを指定できるはずだが、実際に設定アプリで`A`〜`Z`,`0`〜`9`の36文字をセットすると、下記のデータとなった。

![x36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/16d10ff5-10f4-b22c-c7e4-0af95f44b5be.png)

個数は、`36（0x24）`であるが、データが`A`〜`T`までの**20**個しかない。当初は、**26**個を超える場合は、複数ブロックに分割されるのかと想定したが、まさか**20**個で打ち切られるとはまったくの想定外。個数とデータ数に矛盾があり、設定アプリのバグも疑われる。
_Mac用設定アプリが作成できたら、20個を超えるデータをセットしてみて、実際のデバイスの動作を検証する予定。_ **⇒** [検証結果はこちら](#20個を超えるキーコード設定の検証)


## 4) RGBLED設定時のデータ

Layerごとに、`Mode0`〜`Mode5`の6パターンと7色の組み合わせでRGBLEDの光らせ方を指定できる。
![RGBLED.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/137bbb63-e1e6-d9a0-e7e3-00cdb55fb07f.png)

下記は、Layer3に、`Mode3`の`Green`を割り当てた例である。
![led_data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/853d06bb-80b4-8bea-2ad9-d5b7423b11f4.png)

- 1〜3バイト目：03 fe b0固定
- 4バイト目：Layer番号（1〜3）（例では3）
- 5バイト目：08固定（データ種別）
- 11バイト目：01固定（後ろにつづくデータの個数＝1）
- 13バイト目：上4ビット色情報＋下4ビットMode情報
上4ビット：Red〜Purpleが1〜7に対応
下4ビット：Mode0〜Mode5が0〜5に対応（Mode0は消灯指定でColorは無視）

## 5) Mouse設定時のデータ

キー入力と同様にマウスのボタン操作も設定できる。
![mouse_spec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/52567179-4d9f-16f9-1f31-ba944e4aa42b.png)

以下は、Layer1、Key1に`Ctrl+Mouse Down`を割り当てたときのデータ。
![mouse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/b51f2f67-64e1-fe91-4d5b-398bc4aaede4.png)

- 1〜2バイト目：03fd固定
- 3バイト目：Key番号（1〜6、0x10〜0x12）（例では1）
- 4バイト目：Layer番号（1〜3）（例では1）
- 5バイト目：03固定（データ種別）
- 11バイト目：01固定（後ろにつづくデータの個数＝1）
- 12、13、16バイト目：下表に示すデータ（空欄は0x00）

|設定値|12|13|16||
|:-|:-:|:-:|:-:|-|
|Mouse LetfKey||0x01||マウス左ボタンクリック|
|Mouse Middle||0x04||マウス中央ボタンクリック|
|Mouse Right||0x02||マウス右ボタンクリック|
|Mouse Wheel+|||0x01|マウスホイール下回転|
|Mouse Wheel-|||0xFF|マウスホイール上回転|
|Ctrl+Mouse Up|0x01||0x01|Ctrl+マウス左ボタンup|
|Ctrl+Mouse Down|0x01||0xFF|Ctrl+マウス左ボタンdown|
|Shift+Mouse Up|0x02||0x01|Shift+マウス左ボタンup|
|Shift+Mouse Down|0x02||0xFF|Shift+マウス左ボタンdown|
|Alt+Mouse Up|0x04||0x01|Shift+マウス左ボタンup|
|Alt+Mouse Down|0x04||0xFF|Shift+マウス左ボタンdown|

12バイト目は`キー モディファイア マスク`と思われるので、論理和により複数のキーによる修飾も可能か？ また、マウスボタンクリックにも修飾できるのか不明。
整然としたパターンになっておらず、マウスホイールのデータを識別する方法に疑問が残る。
また、マウス操作においては、1つのキーに複数の操作を設定できないようである。


## 6) Multimedia設定時のデータ

![multimedia.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/8686e74f-4a37-1496-830e-521831761218.png)

以下は、Layer1、Key1に`Play/Pause`を割り当てたときのデータ。
![media_data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/4be906d6-bdd1-efc2-02a3-49c4c533b855.png)

- 1〜2バイト目：03fd固定
- 3バイト目：Key番号（1〜6、0x10〜0x12）（例では1）
- 4バイト目：Layer番号（1〜3）（例では1）
- 5バイト目：02固定（データ種別）
- 11バイト目：02固定（後ろにつづくデータの個数＝2）
- 12、13バイト目：下表に示すデータ（空欄は0x00）

|設定値|12|13|備考|
|:-|:-:|:-:|-|
|Play/Pause		|0xcd|||
|Stop			|0xb7|||	
|Previous track	|0xb6|||
|Next track		|0xb5|||
|My Computer		|0x94|0x01||
|Screen britness+|0x6f|||	
|Screen britness-|0x70|||	
|Multimedia		|0x83|0x01|Windows Media Playerが開いた|
|Mute			|0xe2|||	
|Volume+			|0xe9|||
|Volume-			|0xea|||
|Calculator		|0x92|0x01|電卓が開いた|
|WWW home		|0x23|0x02|Edgeが開いた|
|E-mail			|0x8a|0x01||
|Bass+			|0x52|0x01||
|Bass-			|0x53|0x01||
|Treble+			|0x54|0x01||
|Treble-			|0x55|0x01||
|WWW Pagerefresh	|0x27|0x02||
|WWW Pageforward	|0x25|0x02||
|WWW Pageback	|0x24|0x02||




# MacAppコマンドライン版インタフェース確認のみ

コマンドライン版MacAPPを作成したが、インタフェース確認が目的のため、Layer、Key、キーコードなどは、ハードコーディングしている。Xcodeで`Command Line Tool`を選択し、 C言語インタフェースのlibUSBを使用するため、`Project-Bridging-Header`が必要で、Appの実行にはスーパーユーザ権限が必要である。


<details><summary>Project-Bridging-Headerのコードを表示</summary><div>

```c:Project-Bridging-Header.h
#include <libusb.h>
#include <string.h>
#include <errno.h>

//SwiftからC言語の可変長引数の関数が呼び出せないため、デバックログレベル設定専用のブリッジ関数を用意
int libusb_set_option_debug(libusb_context *ctx, enum libusb_option option1, enum libusb_log_level option2) {
    return libusb_set_option(ctx, option1, option2);
}
```

</div></details>

以下は、Layer1、Key1に、"aB"を割り当てる確認プログラム。

<details><summary>mainのコードを表示</summary><div>

```swift:main.swift
//
//  main.swift
//  6key_setting_test
//

import Foundation

func asciiCStringToString(_ cString: [CChar]) -> String {
    var result = ""
    for char in cString {
        if char == 0 { return result }
        result += String(Character(UnicodeScalar(UInt8(char))))
    }
    return result
}

func hexDump(_ cString: [UInt8], length: Int) {
    var buffer = ""
    for offset in stride(from: 0, to: length, by: 1) {
        buffer += String(format: "%@%02x ", offset > 0 && offset.isMultiple(of: 16) ? "\n" : "", cString[offset])
    }
    print(buffer)
}

let VENDOR_ID: UInt16  = 0x1189
let PRODUCT_ID: UInt16 = 0x8840
let INTERFACE: Int32  = 0
let ENABLE: Int32  = 1
let DISABLE: Int32 = 0
let ENDPOINT: UInt8   = 0x04

let main: () = {

    var ctx: OpaquePointer? = nil
    var ret: Int32 = 0

    libusb_init(&ctx)
    ret = libusb_set_option2(nil, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_DEBUG)
    if (ret != 0) {
        print(String(format: "libusb_set_option2() failed. %s", libusb_error_name(ret)))
    }
    
    let handle = libusb_open_device_with_vid_pid(ctx, VENDOR_ID, PRODUCT_ID)
    if (handle == nil) {
        print(String(format: "libusb_open_device_with_vid_pid() failed. errno:%d %s\n", errno, strerror(errno)))
        return
    }
    
    ret = libusb_set_auto_detach_kernel_driver(handle, ENABLE)
    if (ret != 0) {
        print(String(format: "libusb_set_auto_detach_kernel_driver() failed. %s", libusb_error_name(ret)))
        return
    }
    
    var text = [CChar](repeating: 0, count: 256)
    libusb_get_string_descriptor_ascii(handle, 2, &text[0], 256);
    print(String(format: "opened device \"%@\"", asciiCStringToString(text)))
    
    ret = libusb_claim_interface(handle, INTERFACE)
    if (ret != 0) {
        print(String(format: "libusb_claim_interface() failed. %s", libusb_error_name(ret)))
        return
    }
    
    let layer = 1, key = 1
    let modifiers1 = 0, code1 = 4
    let modifiers2 = 0x2, code2 = 5
    var xferd: Int32 = 0
    
    var data1 = [UInt8](repeating: 0, count: 65)
    [0x03, 0xfd, key, layer, 0x01].enumerated().forEach { data1[$0.offset] = UInt8($0.element) }
    zip([0x02, modifiers1, code1, modifiers2, code2], Array(10...14)).forEach { data1[$1] = UInt8($0) }
    hexDump(data1, length: 65)
    ret = libusb_interrupt_transfer(handle, ENDPOINT, &data1, 65, &xferd, 1000);
    if (ret != 0) {
        print(String(format: "1st libusb_interrupt_transfer() failed. %s", libusb_error_name(ret)))
        return
    }
    
    var data2 = [UInt8](repeating: 0, count: 65)
    [0x03, 0xfd, 0xfe, 0xff].enumerated().forEach { data2[$0.offset] = UInt8($0.element) }
    hexDump(data2, length: 65)
    ret = libusb_interrupt_transfer(handle, ENDPOINT, &data2, 65, &xferd, 1000);
    if (ret != 0) {
        print(String(format: "2nd libusb_interrupt_transfer() failed. %s", libusb_error_name(ret)))
        return
    }
    
    libusb_close(handle)
    libusb_exit(ctx)

    print("Completed!")
}()
```

</div></details>


デバッグログ出力した実行結果を示す。

<details><summary>デバッグログ出力結果を表示</summary><div>


```terminal
% sudo ./6key_setting_test
Password:
[timestamp] [threadID] facility level [function call] <message>
--------------------------------------------------------------------------------
[ 0.005980] [001d7d56] libusb: debug [libusb_get_device_list]  
[ 0.005997] [001d7d56] libusb: debug [libusb_get_device_descriptor]  
[ 0.005999] [001d7d56] libusb: debug [libusb_open] open 0.7
[ 0.006034] [001d7d56] libusb: debug [darwin_open] device open for access
[ 0.006137] [001d7d56] libusb: debug [libusb_submit_transfer] transfer 0x129f04470
[ 0.006171] [001d7d56] libusb: debug [libusb_get_next_timeout] no URB with timeout or all handled by OS; no timeout!
[ 0.006174] [001d7d56] libusb: debug [libusb_handle_events_timeout_completed] doing our own event handling
[ 0.006176] [001d7d56] libusb: debug [handle_events] event sources modified, reallocating event data
[ 0.006179] [001d7d56] libusb: debug [usbi_wait_for_events] poll() 1 fds with timeout in 60000ms
[ 0.008358] [001d7d5f] libusb: debug [darwin_async_io_callback] an async io operation has completed
[ 0.008374] [001d7d56] libusb: debug [usbi_wait_for_events] poll() returned 1
[ 0.008379] [001d7d56] libusb: debug [handle_event_trigger] event triggered
[ 0.008381] [001d7d56] libusb: debug [darwin_handle_transfer_completion] handling transfer completion type control with kernel status 0
[ 0.008383] [001d7d56] libusb: debug [usbi_handle_transfer_completion] transfer 0x129f04470 has callback 0x10035c99c
[ 0.008385] [001d7d56] libusb: debug [sync_transfer_cb] actual_length=4
[ 0.008388] [001d7d56] libusb: debug [libusb_free_transfer] transfer 0x129f04470
[ 0.008397] [001d7d56] libusb: debug [libusb_submit_transfer] transfer 0x12b804150
[ 0.008414] [001d7d56] libusb: debug [libusb_get_next_timeout] no URB with timeout or all handled by OS; no timeout!
[ 0.008416] [001d7d56] libusb: debug [libusb_handle_events_timeout_completed] doing our own event handling
[ 0.008416] [001d7d5f] libusb: debug [darwin_async_io_callback] an async io operation has completed
[ 0.008418] [001d7d56] libusb: debug [usbi_wait_for_events] poll() 1 fds with timeout in 60000ms
[ 0.008428] [001d7d56] libusb: debug [usbi_wait_for_events] poll() returned 1
[ 0.008430] [001d7d56] libusb: debug [handle_event_trigger] event triggered
[ 0.008431] [001d7d56] libusb: debug [darwin_handle_transfer_completion] handling transfer completion type control with kernel status 0
[ 0.008432] [001d7d56] libusb: debug [usbi_handle_transfer_completion] transfer 0x12b804150 has callback 0x10035c99c
[ 0.008433] [001d7d56] libusb: debug [sync_transfer_cb] actual_length=42
[ 0.008435] [001d7d56] libusb: debug [libusb_free_transfer] transfer 0x12b804150
opened device "USB Composite Device"
[ 0.008628] [001d7d56] libusb: debug [libusb_claim_interface] interface 0
[ 0.008682] [001d7d56] libusb: debug [darwin_detach_kernel_driver] attempting to detach kernel driver from device
[ 0.008718] [001d7d56] libusb: info [darwin_detach_kernel_driver] no capture entitlements. may not be able to detach the kernel driver for this device
[ 0.010616] [001d7d56] libusb: debug [darwin_reenumerate_device] darwin/reenumerate_device: restoring state...
[ 0.010640] [001d7d56] libusb: debug [darwin_open] device open for access
[ 0.010643] [001d7d56] libusb: debug [darwin_restore_state] darwin/restore_state: reclaiming interfaces
[ 0.010646] [001d7d56] libusb: debug [darwin_restore_state] darwin/restore_state: device state restored
[ 0.011127] [001d7d56] libusb: debug [get_endpoints] building table of endpoints.
[ 0.011134] [001d7d56] libusb: debug [get_endpoints] interface: 0 pipe 1: dir: 1 number: 4
[ 0.011138] [001d7d56] libusb: debug [get_endpoints] interface: 0 pipe 2: dir: 0 number: 4
[ 0.011152] [001d7d56] libusb: debug [darwin_claim_interface] interface opened
03 fd 01 01 01 00 00 00 00 00 02 00 04 02 05 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00  
[ 0.011350] [001d7d56] libusb: debug [libusb_submit_transfer] transfer 0x12b804700
[ 0.011351] [001d7d56] libusb: debug [ep_to_pipeRef] converting ep address 0x04 to pipeRef and interface
[ 0.011353] [001d7d56] libusb: debug [ep_to_pipeRef] pipe 2 on interface 0 matches
[ 0.011385] [001d7d56] libusb: debug [libusb_get_next_timeout] next timeout in 0.999966s
[ 0.011388] [001d7d56] libusb: debug [libusb_handle_events_timeout_completed] doing our own event handling
[ 0.011389] [001d7d56] libusb: debug [usbi_wait_for_events] poll() 1 fds with timeout in 1000ms
[ 0.013647] [001d7d5f] libusb: debug [darwin_async_io_callback] an async io operation has completed
[ 0.013671] [001d7d56] libusb: debug [usbi_wait_for_events] poll() returned 1
[ 0.013679] [001d7d56] libusb: debug [handle_event_trigger] event triggered
[ 0.013682] [001d7d56] libusb: debug [darwin_handle_transfer_completion] handling transfer completion type interrupt with kernel status 0
[ 0.013687] [001d7d56] libusb: debug [usbi_handle_transfer_completion] transfer 0x12b804700 has callback 0x10035c99c
[ 0.013690] [001d7d56] libusb: debug [sync_transfer_cb] actual_length=65
[ 0.013695] [001d7d56] libusb: debug [libusb_free_transfer] transfer 0x12b804700
03 fd fe ff 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 
[ 0.013849] [001d7d56] libusb: debug [libusb_submit_transfer] transfer 0x12b904150
[ 0.013851] [001d7d56] libusb: debug [ep_to_pipeRef] converting ep address 0x04 to pipeRef and interface
[ 0.013852] [001d7d56] libusb: debug [ep_to_pipeRef] pipe 2 on interface 0 matches
[ 0.013879] [001d7d56] libusb: debug [libusb_get_next_timeout] next timeout in 0.999972s
[ 0.013882] [001d7d56] libusb: debug [libusb_handle_events_timeout_completed] doing our own event handling
[ 0.013883] [001d7d56] libusb: debug [usbi_wait_for_events] poll() 1 fds with timeout in 1000ms
[ 0.017607] [001d7d5f] libusb: debug [darwin_async_io_callback] an async io operation has completed
[ 0.017631] [001d7d56] libusb: debug [usbi_wait_for_events] poll() returned 1
[ 0.017637] [001d7d56] libusb: debug [handle_event_trigger] event triggered
[ 0.017640] [001d7d56] libusb: debug [darwin_handle_transfer_completion] handling transfer completion type interrupt with kernel status 0
[ 0.017644] [001d7d56] libusb: debug [usbi_handle_transfer_completion] transfer 0x12b904150 has callback 0x10035c99c
[ 0.017647] [001d7d56] libusb: debug [sync_transfer_cb] actual_length=65
[ 0.017652] [001d7d56] libusb: debug [libusb_free_transfer] transfer 0x12b904150
[ 0.017665] [001d7d56] libusb: debug [libusb_close]  
[ 0.017670] [001d7d56] libusb: debug [libusb_release_interface] interface 0
[ 0.017873] [001d7d56] libusb: debug [darwin_attach_kernel_driver] reenumerating device for kernel driver attach
[ 0.126452] [001d7d56] libusb: debug [darwin_reenumerate_device] darwin/reenumerate_device: waiting for re-enumeration to complete...
[ 0.128381] [001d7d5f] libusb: debug [darwin_devices_detached] detected device detached due to re-enumeration. sessionID: 0x4bed2678b29, locationID: 0x120000
[ 0.162517] [001d7d5f] libusb: debug [darwin_get_cached_device] finding cached device for sessionID 0x4bf81dbca23
[ 0.162554] [001d7d5f] libusb: debug [darwin_get_cached_device] parent sessionID: 0xa18e0b5
[ 0.162557] [001d7d5f] libusb: debug [darwin_get_cached_device] matching sessionID/locationID 0x4bf81dbca23/0x120000 against cached device with sessionID/locationID 0x4bed2678b29/0x120000
[ 0.162561] [001d7d5f] libusb: debug [darwin_get_cached_device] found cached device with matching location that is being re-enumerated
[ 0.162563] [001d7d5f] libusb: debug [darwin_get_cached_device] caching new device with sessionID 0x4bf81dbca23
[ 0.162816] [001d7d5f] libusb: debug [darwin_cache_device_descriptor] cached device descriptor:
[ 0.162820] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bDescriptorType:    0x01
[ 0.162822] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bcdUSB:             0x0110
[ 0.162824] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bDeviceClass:       0x00
[ 0.162826] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bDeviceSubClass:    0x00
[ 0.162828] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bDeviceProtocol:    0x00
[ 0.162829] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bMaxPacketSize0:    0x40
[ 0.162831] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   idVendor:           0x1189
[ 0.162833] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   idProduct:          0x8840
[ 0.162835] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bcdDevice:          0x0100
[ 0.162837] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   iManufacturer:      0x01
[ 0.162839] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   iProduct:           0x02
[ 0.162840] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   iSerialNumber:      0x03
[ 0.162842] [001d7d5f] libusb: debug [darwin_cache_device_descriptor]   bNumConfigurations: 0x01
[ 0.213163] [001d7d5f] libusb: debug [darwin_check_configuration] active config: 1, first config: 1
[ 0.213176] [001d7d5f] libusb: debug [process_new_device] re-using existing device from context 0x129e04890 for with session 0x4bed2678b29 new session 0x4bf81dbca23
[ 0.213181] [001d7d5f] libusb: debug [process_new_device] found device with address 7 port = 2 parent = 0x600003384240 at 0x600003984bba
[ 0.213183] [001d7d5f] libusb: debug [darwin_devices_attached] cached device in reset state. reset complete...
[ 0.213184] [001d7d56] libusb: debug [darwin_reenumerate_device] darwin/reenumerate_device: checking whether descriptors changed
[ 0.213189] [001d7d56] libusb: debug [darwin_reenumerate_device] darwin/reenumerate_device: device reset complete. restoring state...
[ 0.213419] [001d7d56] libusb: debug [darwin_open] device open for access
[ 0.213422] [001d7d56] libusb: debug [darwin_restore_state] darwin/restore_state: reclaiming interfaces
[ 0.213424] [001d7d56] libusb: debug [darwin_restore_state] darwin/restore_state: re-claiming interface 0
[ 0.214969] [001d7d56] libusb: info [darwin_claim_interface] USBInterfaceOpen: another process has device opened for exclusive access
[ 0.214973] [001d7d56] libusb: debug [darwin_restore_state] darwin/restore_state: could not claim interface 0
[ 0.214974] [001d7d56] libusb: info [darwin_capture_release_interface] on attempt to reattach the kernel driver got ret=-5
[ 0.214991] [001d7d56] libusb: debug [libusb_exit]  
[ 0.214999] [001d7d5f] libusb: debug [darwin_event_thread_main] darwin event thread exiting
[ 0.215220] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.7
[ 0.215579] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.6
[ 0.215631] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.5
[ 0.215673] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.4
[ 0.215707] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.3
[ 0.215865] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.2
[ 0.215917] [001d7d56] libusb: debug [libusb_unref_device] destroy device 0.1
[ 0.215955] [001d7d56] libusb: debug [libusb_unref_device] destroy device 2.3
[ 0.215995] [001d7d56] libusb: debug [libusb_unref_device] destroy device 2.2
[ 0.216033] [001d7d56] libusb: debug [libusb_unref_device] destroy device 2.1
[ 0.216070] [001d7d56] libusb: debug [usbi_remove_event_source] remove fd 3
Completed!
% aB
```

</div></details>

実際にLayer1、キー1を押すと、"aB"がタイプされ、正しく設定したことが確認できた。

## 20個を超えるキーコード設定の検証

36個のキーコードを指定してもデバイスに20個のデータしか渡していないのは、設定ソフトの仕様（バグ？）であった。上記で作成した検証アプリにて 26個のデータを設定して、実際のデバイスの動作を確認した。

送信したデータは、以下の通り。Layer1、Key1に`A`〜`Z`の26個（0x1a）のキーコード。


```console
03 fd 01 01 01 00 00 00 00 00 1a 00 04 00 05 00 
06 00 07 00 08 00 09 00 0a 00 0b 00 0c 00 0d 00 
0e 00 0f 00 10 00 11 00 12 00 13 00 14 00 15 00 
16 00 17 00 18 00 19 00 1a 00 1b 00 1c 00 1d 00 
00
```

送信は正常に完了した。実際にLayer1、Key1を押してみると、`abcdefghijklmnopqr`がタイプされた。先頭の18文字だけ?!。このデバイスは、1つのキーに付き18個までしかメモリされない仕様と推察した。


## デバイスから設定情報を読み出す

設定アプリには、「Reading Device」というボタンが用意されていて、デバイスに設定されている情報を読み出すことができる。

データの呼び出しは、Layerごとに以下のデータで指定する。

- 5バイト目：：Layer番号（1〜3）（例では3）

![readingDevice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/ea8dc753-c5fb-1ff4-d076-385404a632f3.png)

このデータを送信後、デバイスから9組のデータが連続で送信されてくる。ボタンのキー6個とノブのキー3個の情報で、形式は設定時と同一。（RGBLEDの設定情報は読み出しできない）

読み出しも、確認プログラムにて問題なく実行できた。


## デバイスから設定情報を読み出す（その2）

設定アプリは起動時に、下記フォーマットのデータをデバイスに送信している。すると、デバイスから回答データを受信する。
![startup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/63bc4424-c7ca-3d81-c2e3-d4a98a7cfce5.png)

このデータのやり取りは、デバイスの接続確認と、接続したデバイスが持つキーとノブの個数を確認しているものと想定する。キー9/12個＋ノブ3個など、このデバイスには兄弟デバイスが何種類かある。上記はキー6個＋ノブ1個で本デバイスと一致するが、他にデバイスを持っていないため、真偽は不明である。


# 調査編のまとめ

- 設定インタフェースを真似る方針に変更
- Windows10にWinsharkとUSBPcapをインストールして、USBデータをキャプチャできた
- キャプチャデータを解析して、各種設定データのフォーマットが判明した
- MacのコマンドラインAppにて、解析したデータ形式で実際に設定できることを確認した
- デバイスからの設定情報の読み出しも、確認アプリで動作確認した

以上により、調査としてはほぼ目的を達成できた。これで、元々の購入目的である、MacAppによる設定アプリの作成に着手できるが、そもそもこのデバイスが次の点でイマイチであるので、最終的には、このマイコンのプログラムを自分好みに書き換えるつもりである。

**イマイチポイント**
- 1つのキーに設定できるキーコード数が少なすぎる
- マウス操作がボタン操作に限定されていて、（ホイールの回転はあるのに、）マウス移動が使えない
- また、マウス操作とキーボード操作の組み合わせが使えない


しかし、CH573マイコン用の開発環境を整備することに相当な時間を要すと考えられるため[^6]、その間に、Mac版設定アプリも作成しておく。続編は別記事で投稿する予定。


今回の記事は以上です。

<br><hr>

こちらのブログを参考にさせていただきましたが、データ形式がかなり異なっていました。見た感じは同じ仲間と思いましたが・・・

https://neocat.hatenablog.com/entry/2022/10/22/233222

<br><br>


[^1]:[プログラマブルテンキーを作る《https://qiita.com/nak435/items/71e8c6ac306a661f8be8》](https://qiita.com/nak435/items/71e8c6ac306a661f8be8)
[^2]:[ロータリーエンコーダーを使った回転型デバイスの製作《https://qiita.com/nak435/items/44897e080ab51208ca3c》](https://qiita.com/nak435/items/44897e080ab51208ca3c)
[^3]:[【備忘録】Windows10 102/109キーボードの変更手順《https://qiita.com/nak435/items/25adbfb17285db7e35fc》](https://qiita.com/nak435/items/25adbfb17285db7e35fc)
[^4]:[Raspberry Pi Desktop「USキーボード」の設定方法《https://qiita.com/nak435/items/5ba4b1254b3b7d0c0265》](https://qiita.com/nak435/items/5ba4b1254b3b7d0c0265)
[^5]:[Arduino HID-Projectを日本語キーボードに対応する《https://qiita.com/nak435/items/bbe04300c67c37febb7e》](https://qiita.com/nak435/items/bbe04300c67c37febb7e)

[^6]:`MounRiver Studio(MRS)`という統合開発環境だが、サイト<http://www.mounriver.com/>~~が閉じられていて、新規での環境構築は不可能かも？~~ にpingは通るので、中国在住の友人にダウンロードを依頼し入手した

[^7]:(2023.12.22追記)久々に商品ページを見たら、Macの設定ソフトが追加されていた。[《https://sikaicase.com/blogs/news/sikai-case-mini-keyboard-software-setting-japanese》](https://sikaicase.com/blogs/news/sikai-case-mini-keyboard-software-setting-japanese)