本blogはGoogle AdSenseおよび各種アフィリエイト広告を含みます

F(x)tec Pro1-Xのファームウェアイメージを解体し、キーマップ定義を確認する

半年くらい前からInertial Driftの日本語をもう少し読みやすく改変できないものかとUnityゲームのModの作り方を調べています。やり方の半分は理解したので、次の半年くらいで具体的に動くものを出してみたいですね(勿論それまでに翻訳が良くなってれば言うことないけど、まあこういうとこでインディーゲームが大変なのも分かる……)。

いや、今回はモノになるか分からんModではなく実用可能なModの話です。夏頃にやっていたことのもう一つ、おととし作ったAndroid用キーマップ追加アプリFx Kanji Modの更新でやったことを書いていきます。

成果物と、おさらい

Android OSでは接続されている物理キーボードの調整のため、キーマッピング(Key Character Mapping)設定をアプリケーションパッケージとして配布することが認められています。

↑これに、

↑こういう機能を足したい、などができるわけです。

play.google.com

できあがったものがこちらにあります。

eps-r.hatenablog.com

Fx Kanji Modとは、F(x)tec Pro1(QX1000)およびPro1-X(QX1050)のビルトインキーボードのために、Fn・Alt・FxLogo(※WindowsキーやCmdキーに相当)の挙動を調整するとともに、日本語入力に使用しないSymキーを半角/全角漢字キーに置き換えるための私製パッケージです。基本的な挙動はFork元のFx Qwertyに準じます。

端的には、ノートPCっぽいFn+カーソルキーなどのバインドが有効になり、ATOK・Google日本語入力など一部IMEでのSymキーでの日本語英語切り替えが可能になるものです(Gboardはだめ)。

F(x)tec Pro1-X(QX1050)が発売された

ということで、普通に使うぶんには上記リンクからFx Kanji Modをインストールしていただいて物理キーボードレイアウトを追加・選択していただければと思います。

以下はこのFx Kanji Modの2年ぶりの更新点、新機種Pro1-Xへの対応についてやったことの詳細を書いていきます。

私が実機を未だ入手していないので、SoCなどのスペックや全体的な変更点について述べるのは避けますが、延期を1年以上繰り返しただけあってそのキーレイアウトは変遷を重ねており、全ての前情報が信用ならないことになっています。

これらが全部食い違っている。Pro1の配列に対してPro1-Xで論理的・物理的にどのような改変が加えられているのか全く不明瞭です。せめてプレスキットや商品ページには最終版の写真を載せておいてほしい。

akiba-pc.watch.impress.co.jp

発売後にユーザーから上がってきたtwitterの着荷報告や、上記AKIBA PC Hotlineの店頭量産機の画像などは信用できるものですが、これもいざ起こしてみると結構なことになっており、Shiftを押さずに "?" の打てるキーボードとはどういう物体なのか、という疑問を解決しなければ更新できないと考えました。

ファームウェアを探し、解体する

という流れから記事タイトル通りの、ファームウェア云々の話になっていきます。

ダウンロードする

community.fxtec.com

community.fxtec.com

オフィシャルフォーラムにそれぞれPro1用、Pro1-X用のファームウェアのリンクがあるので、これが純正であると信じることにします。

解体する

あとはそれぞれのzipファイルを展開してしまえば終わりだと思っていたのですが、そう単純ではなく、

source.android.com

ファイル システム。これらのイメージには system、userdata、recovery イメージがあり、Yaffs2 形式またはスパース イメージ形式を使用する必要があります。

どうもzipファイル中の vendor.img や system.img はスパース イメージ形式というものになっているらしく、知っている汎用のツールだとあまりうまく扱えません。

色々検索していると、xda-developersを中心にsgs2ext4というJavaアプリであったり、幾つかそれらしいツールや解説記事が引っ掛かってきます。

今回は下記のFirmware_ExtractorというのをWSL2上で使ってみることにしました。

github.com

### インストール
$ git clone --recurse-submodules https://github.com/AndroidDumps/Firmware_extractor.git
$ cd Firmware_extractor/
$ sudo apt install python3-pip
$ sudo apt install unace unrar zip unzip p7zip-full p7zip-rar sharutils rar uudeview mpack arj cabextract rename
$ sudo apt install liblzma-dev brotli lz4 --fix-missing
$ pip install backports.lzma protobuf pycrypto twrpdtgen extract-dtb

### 実行 Pro1
$ ./extractor.sh /mnt/g/Downloads/ext4_QX1000_user_20200825231445_dd49dd0dd1.7z
$ mv -f ./out /mnt/g/Downloads/QX1000

### 実行 Pro1-X
$ ./extractor.sh /mnt/g/Downloads/merged-qssi_bengal-ota.zip
$ mv -f ./out /mnt/g/Downloads/QX1050

パスは適当に読み替えていただくとして、こんな感じで中身が作業ディレクトリ配下の./out/に出力されていきます。

キーマップ定義を確認する

Firmware_extractorを通した後はSparse Imageファイルがext4のディスクイメージに変換され、7-Zipあたりでそのまま開けるようになります。

あとはAndroidのドキュメントのキー レイアウト ファイルキー文字マップファイルに従い、イメージとパスを見ていくだけです。

結果として、ベースとなるレイアウト定義であるklファイルと、その上に被せて同時押しなどを定義するkcmファイルは下記の位置にありました。

Pro1(QX1000): system.img
/system/usr/keylayout/Vendor_181d_Product_5018_Version_0001.kl
/system/usr/keychars/Vendor_181d_Product_5018_Version_0001.kcm

Pro1(QX1050): system.img
/system/usr/keylayout/Generic.kl
/system/usr/keylayout/Generic.kcm

キーレイアウト(*.kl)はどちらも外付け用の基本的なキーレイアウト(Generic.kl)から1バイトも変更はなく、もっぱらキーキャラクターマップ(*.kcm)が変わっています。

--- Pro1/keychars/Vendor_181d_Product_5018_Version_0001.kcm  2009-01-01 01:00:00.000000000 +0900
+++ Pro1-X/keychars/Generic.kcm   2009-01-01 09:00:00.000000000 +0900
@@ -97,9 +97,9 @@
 key L {
     label:                              'L'
     base:                               'l'
     shift, capslock:                    'L'
-    fn:                                 '?'
+    fn:                                    ';'
 }
 
 key M {
     label:                              'M'
@@ -123,9 +123,9 @@
 key P {
     label:                              'P'
     base:                               'p'
     shift, capslock:                    'P'
-  fn:                                 '/'
+    fn:                                 '\\'
 }
 
 key Q {
     label:                              'Q'
@@ -191,71 +191,82 @@
 
 key 0 {
     label:                              '0'
     base:                               '0'
+    shift:                              none
     fn:                                 ')'
 }
 
 key 1 {
     label:                              '1'
     base:                               '1'
+    shift:                              none
     fn:                                 '!'
 }
 
 key 2 {
     label:                              '2'
     base:                               '2'
+    shift:                              none
     fn:                                 '@'
 }
 
 key 3 {
     label:                              '3'
     base:                               '3'
+    shift:                              none
     fn:                                 '#'
 }
 
 key 4 {
     label:                              '4'
     base:                               '4'
+    shift:                              none
     fn:                                 '$'
 }
 
 key 5 {
     label:                              '5'
     base:                               '5'
+    shift:                              none
     fn:                                 '%'
 }
 
 key 6 {
     label:                              '6'
     base:                               '6'
-    fn:                                 '^'
+    shift:                              none
     alt+shift:                          '\u0302'
+    fn:                                 '^'
 }
 
 key 7 {
     label:                              '7'
     base:                               '7'
+    shift:                              none
     fn:                                 '&'
 }
 
 key 8 {
     label:                              '8'
     base:                               '8'
+    shift:                              none
     fn:                                 '*'
 }
 
 key 9 {
     label:                              '9'
     base:                               '9'
+    shift:                              none
     fn:                                 '('
 }
 
 key SPACE {
     label:                              ' '
     base:                               ' '
     alt, meta:                          fallback SEARCH
     ctrl:                               fallback LANGUAGE_SWITCH
+    fn:                                 fallback LANGUAGE_SWITCH
 }
 
 key ENTER {
     label:                              '\n'
@@ -269,70 +280,81 @@
 
 key COMMA {
     label:                              ','
     base:                               ','
+    shift:                              none
     fn:                                 '<'
 }
 
 key PERIOD {
     label:                              '.'
     base:                               '.'
+    shift:                              none
     fn:                                 '>'
 }
 
 key SLASH {
     label:                              '/'
     base:                               '/'
-    shift:                              '?'
+    shift:                              none
+    fn:                                 '|'
 }
 
 key GRAVE {
     label:                              '`'
     base:                               '`'
-    fn:                                 '~'
+    shift:                              none
     alt:                                '\u0300'
     alt+shift:                          '\u0303'
+    fn:                                 '~'
 }
 
 key MINUS {
     label:                              '-'
     base:                               '-'
+    shift:                              none
     fn:                                 '_'
 }
 
 key EQUALS {
     label:                              '='
     base:                               '='
+    shift:                              none
     fn:                                 '+'
 }
 
 key LEFT_BRACKET {
     label:                              '['
     base:                               '['
+    shift:                              none
     fn:                                 '{'
 }
 
 key RIGHT_BRACKET {
     label:                              ']'
     base:                               ']'
+    shift:                              none
     fn:                                 '}'
 }
 
 key BACKSLASH {
     label:                              '\\'
     base:                               '\\'
+    shift:                              none
     fn:                                 '|'
 }
 
 key SEMICOLON {
-    label:                              ';'
-    base:                               ';'
+    label:                              '?'
+    base:                               '?'
+    shift:                              none
     fn:                                 ':'
 }
 
 key APOSTROPHE {
     label:                              '\''
     base:                               '\''
+    shift:                              none
     fn:                                 '"'
 }
 
 ### Numeric keypad ###
@@ -541,9 +563,9 @@
     base:                               fallback MENU
 }
 
 key BUTTON_MODE {
-    base:                               fallback MENU
+    base:                               fallback HOME
 }
 
 key BUTTON_1 {
     base:                               fallback DPAD_CENTER

行数は多いですが、要点はそれほどでもありません。影響するのはL、P、SLASH、SEMICOLONだけです。

抜粋すると、こうなります。

 key L {
     label:                              'L'
     base:                               'l'
     shift, capslock:                    'L'
-    fn:                                 '?'
+    fn:                                    ';'
 }

 key P {
     label:                              'P'
     base:                               'p'
     shift, capslock:                    'P'
-  fn:                                 '/'
+    fn:                                 '\\'
 }

 key SLASH {
     label:                              '/'
     base:                               '/'
-    shift:                              '?'
+    shift:                              none
+    fn:                                 '|'
 }

 key SEMICOLON {
-    label:                              ';'
-    base:                               ';'
+    label:                              '?'
+    base:                               '?'
+    shift:                              none
     fn:                                 ':'
 }

Pro1とPro1-Xの変更点を把握したところで、Mod(*.kcm)側を修正していきます。

成果を拡張キーマップに反映する

Fx Kanji Modでは先の変更を追いかけ、下のような内容で更新しました

  • Fn+Pをスラッシュからバックスラッシュに変更
  • Fn+Lをハテナからセミコロンに変更 -Fn(or Shift)+スラッシュをハテナからパイプに変更
  • セミコロン単独押しをハテナに変更

その後は実機を持っているかたにテストビルドの動きをtwitterで確認して貰ったり(本当ありがたいことです)などしたのち、ストアにアップロードしたりして今に至ります。

おわり

Playストア経由のインストール数を見る限り、Pro1-X発売直前から2ヶ月ちょっと経った現在インストール数が2倍弱くらいまで上がっているので(それでも100行かないくらいですが)、対応してとてもよかったと思います。コメント欄Twitterでの報告などがあるとやる気が上がっていくものですね。

以上、Androidのシステムファイルをrootやブートローダアンロックどころか実機も持たない状態で覗くことができてちょっと面白かった、という話でした。

余談

繰り返しになりますがPro1-Xに関してはエアプなので、大きな口は叩けない状態にあり、あるのですが。

(現行Pro1-Xのレイアウト、Pro1からの変更点)

  • Fn+Pをスラッシュからバックスラッシュに変更
  • Fn+Lをハテナからセミコロンに変更 -Fn(or Shift)+スラッシュをハテナからパイプに変更
  • セミコロン単独押しをハテナに変更
[-_] [=+]
[P\] []{]] []}]
[L:] [?:] [' "]
[`~] ... [,<] [.>] [/|] 

……という現行レイアウトに対して、

(こうしたほうがよくない? というPro1-Xのレイアウト)

  • Fn+Pをスラッシュからバックスラッシュに変更
  • Fn+Lをハテナからパイプに変更
[-_] [=+]
[P\] []{]] []}]
[L|] [;:] [' "]
[`~] ... [,<] [.>] [/?]

結局足りないキーは1個だけだし、大胆な再配置よりもバックスラッシュ(とパイプ)だけをバラして、他はUSレイアウトに揃えるほうがうれしくないか……? と思っていて、キーキャラクターマップを更に足したい気持ちもありますが、実機を持っていないのと「キートップにウソをつくのはキモい(書いてある字が打てない)」というポリシーが勝って結局入れていません……。