tmytのらくがき

個人の日記レベルです

どうやらこれは自作キーボードというカテゴリらしい

自作キーボードというほどではないのだけど、yonkeyというのをちょっと前に作った。テンキーの右下をちぎったのでyonkey。

f:id:tmyt:20210624040704p:plain

テンキーの 0 と Enterが欲しかっただけで、後の2個はおまけ。何に使うのかというと、FF14のタイトル画面でテンキーの0を連打するためにある*1

スイッチもそれぞれがGPIOに直結されている素朴な実装。自作キーボードというにはおこがましいこのyonkeyは、コントローラはSeeeduino Xiaoで、ファームウェアArduinoで手書き。

そんなyonkeyを作って満足してたところでこんなん作ろうよーって言われたのでとりあえず作ってみようの会

ふむなるほど、なるほど…ってKiCad起動して回路図書いて、ちょこちょこ配線して、ときどきFreeroutingでさぼって…シリコンハウスで売ってる部品からフットプリント選んで…寝て起きて見直したらダイオードの向き逆なことに気付いて…FusionPCBで製造して…そんなこんなでできたのがこれ!!名前はtarteletteっていうそうです。

f:id:tmyt:20210624042346p:plain

コントローラはSeeeduino Xiao、Kailhのソケットでスイッチを接続、SK6812MINI-Eが7個乗せられる感じに仕上がりました。それではさっそく部品を実装して…

f:id:tmyt:20210624042814p:plain

事前に準備していたアクリル板を合体させてスイッチをはめて…ファームを書いてできたのがこれ!

f:id:tmyt:20210624042914p:plain

めっちゃ光る!!!!

f:id:tmyt:20210624042933p:plain

まだ6枚も残ってるけど使い道はもうない…

f:id:tmyt:20210624043643p:plain

*1:テンキーの0がOKボタン

GitHubからAzureにCDするのが一瞬でできて感動したというだけの話

GitHubからASP.NET CoreなアプリケーションをAzure Web Appにデプロイするのにビルドとか面倒だなと思っていたら、Azure Portalから一瞬で設定ができて感動した。

  1. 左のメニューから”デプロイ センター”を選ぶ
  2. 右の画面から”設定”を開く
  3. ソースを ”GitHub”にする
  4. ”承認”みたいなボタンが出るので、押してGitHubでログインする
  5. 組織、リポジトリ、ブランチを順番にたどってデプロイしたいリポジトリを選ぶ
  6. ビルドは各自いい感じにする
  7. ”保存”を押す

f:id:tmyt:20210210054209p:plain

これでおわり。自動でGitHub Actionsの設定がリポジトリにコミットされて、自動でビルドからデプロイまでが実行される。

f:id:tmyt:20210210053845p:plain

一瞬で設定できてすごいね。というだけの話。

Surface PenのボタンでSurface Duoのスクリーンショットを撮る

Surface PenはSurface Duoからはキーボードに見えていて、短押しと長押しでそれぞれ特定のスキャンコードを出力するので、Surface Penに設定されているキーレイアウトをカスタムしてPrintScreen*1に割り当てましょう。という話です。

スキャンコードに対応するキーコードを入れ替える

今回は短推しで出力されるキーコード"291"を"SYSRQ"に入れ替えることを目標にします。

Androidに接続されているキーボードは、設定の物理キーボードのレイアウト設定*2で変更できます。しかも、ここのレイアウトはAPKで後から追加できます。じゃぁ作るしかない。

キーボードレイアウトを追加するAPKは、キーキャラクタマップ(KCM)をはじめとしていくつかのファイルが必要です。といっても大したことはないのでAndroidのソースツリーにあるやつをコピペして使うのでほぼ問題ないです。

具体的には、キャラクタマップ、キーレイアウトXML、BroadcastReceiverと、AndroidManifestです。ほかはローカライズで使ってたりするだけなので不要。しいて言えばアイコンぐらいはいるかもしれない。

キャラクタマップの書き方がわからない

書き方は、SDKドキュメントに書いてます。

書いてますがたいして参考になりません。今回みたいな文字を出力しないキーのことはさっぱりわからないのでこのファイルを解釈しているソースを読みます。

このファイルを読んでいくとパーサーの実装が見つかります。

String8 keywordToken = mTokenizer->nextToken(WHITESPACE);
if (keywordToken == "type") {
    mTokenizer->skipDelimiters(WHITESPACE);
    status_t status = parseType();
    if (status) return status;
} else if (keywordToken == "map") {
    mTokenizer->skipDelimiters(WHITESPACE);
    status_t status = parseMap();
    if (status) return status;
} else if (keywordToken == "key") {
    mTokenizer->skipDelimiters(WHITESPACE);
    status_t status = parseKey();
    if (status) return status;
} else {

行頭に現れる値は type, map, key の3種類だそうです。typeの引数は次の6種類があるようです。

  • NUMERIC
  • PREDICTIVE
  • ALPHA
  • FULL
  • SPECIAL_FUNCTION
  • OVERLAY

このtypeOVERLAYにしておくのがいいらしいです。

次に、map。これは次のような構文らしいです。

map key usage {usage code} {key code}
map key {scan code} {key code}

usageが付くパターンは、HIDのUsageを指定するそうですがよくわかりません。ここに

上位の 16 ビットで HID 使用状況ページを、下位の 16 ビットで HID 使用状況 ID を示します。

と書いてあるけれどもそれがどう動くのかはよくわからんやつです。

今回やりたいのはusageが付かない2個目のほう。これを使うと任意のスキャンコードを任意のキーコードに書き換えられます。 たとえばCapslockを左Ctrlに置き換えるとかもこれでできますね。

一番後ろのkey codeはキーコードの名前を指定するとよさそうです。この時指定できる名前は、InputEventLabels.hに定義されてます。

ここを見れば一覧が書いてます。KEYCODE_ってつかないタイプのキーの名前を書けばいい感じですね。

残った、key、これはドキュメントに書いてあるので読んでください。

キャラクタマップを書く

書き方さえわかれば後は簡単。

type OVERLAY
map key 190 SYSRQ

これでおわり。あとはXMLとか用意してAPKにすれば、Surface Penの後ろのボタンでスクリーンショットが取れます。ただ30秒で切断されて反応しなくなるのであんまり役には立たないですね。

今回のソースコードはこちら。

*1:内部的にはSYSRQ

*2:場所は端末によってまちまち、だいたい言語設定の近く

Surface Duoに接続したSurface Penのボタンをイベントで受ける

Surface DuoはいちおうSurfaceと言ってるだけあって、Surface Pen(Slim Pen含む)に対応してます。サイドボタンなどなども一応ちゃんと動くそうです。

さて、このペンがAndroidにどう認識されているのかとても気になるので確認してみます。

$ getevent -p
add device 1: /dev/input/event6
  name:     "sm8150-tavil-snd-card Button Jack"
  events:
    KEY (0001): 0072  0073  00e2  0104  0105  0246
  input props:
    <none>
add device 2: /dev/input/event5
  name:     "sm8150-tavil-snd-card Headset Jack"
  events:
    SW  (0005): 0002  0004  0006  0007  0010  0011  0012  0013
  input props:
    <none>
add device 3: /dev/input/event4
  name:     "surface_touchscreen"
  events:
    KEY (0001): 0141  014b
    ABS (0003): 002f  : value 0, min 0, max 11, fuzz 0, flat 0, resolution 0
                0030  : value 0, min 0, max 21067, fuzz 0, flat 0, resolution 0
                0031  : value 0, min 0, max 21067, fuzz 0, flat 0, resolution 0
                0034  : value 0, min -90, max 90, fuzz 0, flat 0, resolution 0
                0035  : value 0, min 0, max 17709, fuzz 0, flat 0, resolution 0
                0036  : value 0, min 0, max 11411, fuzz 0, flat 0, resolution 0
                0037  : value 0, min 0, max 15, fuzz 0, flat 0, resolution 0
                0039  : value 0, min 0, max 65535, fuzz 0, flat 0, resolution 0
                003a  : value 0, min 0, max 65535, fuzz 0, flat 0, resolution 0
  input props:
    INPUT_PROP_DIRECT
add device 4: /dev/input/event3
  name:     "surface_tail_button"
  events:
    KEY (0001): 007d  00bc  00bd  00be
  input props:
    <none>
add device 5: /dev/input/event1
  name:     "da7280-haptic"
  events:
    FF  (0015): 0050  0051  0052  005d  0060
  input props:
    <none>
add device 6: /dev/input/event0
  name:     "qpnp_pon"
  events:
    KEY (0001): 0072  0074
  input props:
    <none>
add device 7: /dev/input/event2
  name:     "gpio-keys"
  events:
    KEY (0001): 0073
  input props:
    <none>

"surface_tail_button" なんて気になる名前があります。とりあえずペンで画面をタップします。

/dev/input/event4: 0003 002f 0000000a
/dev/input/event4: 0003 0039 0000ffff
/dev/input/event4: 0003 0035 00002c7f
/dev/input/event4: 0003 0036 000018ae
/dev/input/event4: 0000 0000 00000000
/dev/input/event4: 0003 0035 00002c82

ペンは /dev/input/event4 なので、 "surface_touchscreen" で処理されていました。ペンの両端とも反応します。後のボタンは反応しません。

後ろボタンの行方

ペンの後ろのボタンは"surface_tail_button" で来ると思った…けれどもどうやら違うようです。ペンの後ろボタンは実はBluetooth接続されています。あのボタンだけ。 ペンを接続するために、後ろのボタンを5秒間長押しします。すると、ペンが接続されて新しいデバイスが出現します。

add device 1: /dev/input/event7
  name:     "Surface Slim Pen"
  events:
    KEY (0001): 0001  0002  0003  0004  0005  0006  0007  0008
                0009  000a  000b  000c  000d  000e  000f  0010
                0011  0012  0013  0014  0015  0016  0017  0018
                0019  001a  001b  001c  001d  001e  001f  0020
                0021  0022  0023  0024  0025  0026  0027  0028
                0029  002a  002b  002c  002d  002e  002f  0030
                0031  0032  0033  0034  0035  0036  0037  0038
                0039  003a  003b  003c  003d  003e  003f  0040
                0041  0042  0043  0044  0045  0046  0047  0048
                0049  004a  004b  004c  004d  004e  004f  0050
                0051  0052  0053  0056  0057  0058  0060  0061
                0062  0063  0064  0066  0067  0068  0069  006a
                006b  006c  006d  006e  006f  0074  0075  0077
                007d  007e  007f  00b7  00b8  00b9  00ba  00bb
                00bc  00bd  00be
    ABS (0003): 0028  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0029  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                002a  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                002b  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                002c  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                002d  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                002e  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                002f  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0030  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0031  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0032  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0033  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0034  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0035  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0036  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0037  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0038  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                0039  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                003a  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                003b  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                003c  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                003d  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                003e  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
                003f  : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
    MSC (0004): 0004
  input props:
    <none>

これが後ろのペンに反応する入力デバイスだそうです。これはキーボードに見えていて、ボタンを押すとキーコードを出力します。

操作 キーコード
短押し 291
長押し 289

しかも、ACTION_UPだけきます。

…と、ここまではつながってさえいればアプリに新機能組み込んだりいろいろできそうだね!で終わるのですがちゃんとオチを用意しました。

このボタン、30秒無操作でBluetoothが切断します。30秒以内にボタンを押せば一応延長されます。でも無操作30秒で切断されます。再接続は5秒長押しです。というわけであんまり役に立ちません…

ところで

"surface_tail_button" は何だったんだろう…古いSurface Penだとあそこが反応する…?いやそんなまさかね…

Android 10以降で表示されるバッテリ残り時間を取得する

たぶんAndroid 10から追加されたバッテリーの残り時間をアプリで取得できたのでメモ。

Settings.Globalに格納されていて、ContentResolverでアクセスできる。キーはandroid.provider.Settingsに定義があったけれど、@hideされていたのでコピペして使った。

private val BATTERY_ESTIMATES_LAST_UPDATE_TIME = "battery_estimates_last_update_time"
private val TIME_REMAINING_ESTIMATE_MILLIS = "time_remaining_estimate_millis"
private val TIME_REMAINING_ESTIMATE_BASED_ON_USAGE = "time_remaining_estimate_based_on_usage"
private val AVERAGE_TIME_TO_DISCHARGE = "average_time_to_discharge"

private val AVERAGE_TIME_TO_DISCHARGE_UNKNOWN = -1
private val ESTIMATE_MILLIS_UNKNOWN = -1

data class Estimate(
    val lastUpdateTime: Long,
    val estimateMillis: Long,
    val isEstimateBasedOnUsage: Boolean,
    val timeToDischarge: Long,
)
fun getEstimate() = Estimate(
    Settings.Global.getLong(
        contentResolver,
        BATTERY_ESTIMATES_LAST_UPDATE_TIME, -1
    ),
    Settings.Global.getLong(
        contentResolver,
        TIME_REMAINING_ESTIMATE_MILLIS,
        ESTIMATE_MILLIS_UNKNOWN.toLong()
    ),
    Settings.Global.getInt(
        contentResolver,
        TIME_REMAINING_ESTIMATE_BASED_ON_USAGE, 0
    ) == 1,
    Settings.Global.getLong(
        contentResolver,
        AVERAGE_TIME_TO_DISCHARGE,
        AVERAGE_TIME_TO_DISCHARGE_UNKNOWN.toLong()
    )
)

Surface Duo向けアプリ2個をPlayストアに公開しておきました

Surface Duo向けのアプリ2個をPlayストアに公開しておきました。

Duo BrowserHelper

ひとつめ。G8X BrowserHelperのSurface Duo版。

Chrome Custom Tabs互換のブラウザアプリのふりをしてインテントを受け取って、常に別画面でブラウザを開くためのもの。Surface Duoの画面を折りたたんで、半画面で使っているときはCustom Tabsを起動するような仕組みになっています。

アイコンまでコピペなのでそのうちなんとかします。

play.google.com

スクショカッター

ふたつめ。Surface Duoのスクショを片画面分のサイズで切り抜くだけのもの。

Surface Duoは画面がどんな状態になっていても2枚つながったスクショを保存するので、半画面で使ってると空白のディスプレイ部分もくっついてくる。これを、スクショ通知の共有ボタンからこのアプリで開くことで簡単に半分サイズのスクショに編集できるというやつです。

名前がダサい?僕もそう思います。

play.google.com

startActivityの挙動

Surface DuoでのstartActivityの挙動をメモしたものです。基本的にはドキュメントに書いてある内容です。

定義

  • それぞれの画面はペインと呼びます。
  • ホーム画面が表示されているペインは空き状態と呼びます。
  • なにかアプリが起動しているペインは占有状態と呼びます。

挙動

  • ペインに表示されいてるアプリがstartActivityを呼ぶと、同じペインにアクティビティが表示されます
    • ただし、Intent.FLAG_ACTIVITY_NEW_TASKが設定されているかつ、もう一方のペインが空き状態の場合は空きペインに表示されます
      • ただし、もう一方のペインが空き状態でない場合は、呼び出したアクティビティと同じペインに表示されます

表にしておきます。

左側ペインにアプリAを起動して、異なるパッケージに属するアクティビティをstartActivityしたことを想定。

Flags 右側ペインの状態 起動場所
なし 空き
なし 占有
FLAG_ACTIVITY_NEW_TASK 空き
FLAG_ACTIVITY_NEW_TASK 占有

同じパッケージに属するアクティビティをstartActivityする場合はIntent.FLAG_ACTIVITY_MULTIPLE_TASKを付けないと、常に同じペインで表示されます。