読者です 読者をやめる 読者になる 読者になる

tmytのらくがき

個人の日記レベルです

Surface Dialを2個接続してSOUND VOLTEXごっこした人へ

雑記

Surface Dialを2個以上接続していたとしても、正規のAPIからアクセスするといくつあっても1個にしか見えないのは周知の事実です。じゃぁどうにかして、2個認識できないのかなぁ…ということでやってみました。

TL;DR

  • 通常のAPIからは1個しかみえないが、HIDデバイスなので直接読めば読める
  • VID:045E, PID: 091B, UsagePage: 0001, Usage: 000Eを読めば生データ見える
  • 生データの2バイト目の下位1bitが押し下げフラグ、3バイト目が回転量(signed)、4バイト目が回転方向(00:右、FF:左)

バイス構成

f:id:tmyt:20161127132805p:plain

これを見ると、HID over GATTで5種類のデバイスが見えていて、UsagePage, Usageは次の通り

UsagePage Usage
0001 0080
0001 000E
0001 0072
FF07 0070
FF07 0071

一番上はWinRTでブロックされているUsageなので今回は省略。ほかの4つは特にWinRTでブロックされてない(!!)のでアクセスしてみたところ、返事が返ってきたのは2個目のやつでした。

とりあえずアクセス

public MainPage()
{
    this.InitializeComponent();

    Loaded += async (sender, args) =>
    {
        var d2 = await GetAsync(0x0001, 0x000E);
        d2.InputReportReceived += D2_InputReportReceived;
    };
}

private void D2_InputReportReceived(HidDevice sender, HidInputReportReceivedEventArgs args)
{
    var dump = string.Join(" ", args.Report.Data.ToArray().Select(b => $"{b:X2}"));
    Debug.WriteLine($"D2: {dump}");
}

async Task<HidDevice> GetAsync(ushort up, ushort uid)
{
    var str = HidDevice.GetDeviceSelector(up, uid);//, 0x045E, 0x091B);
    var devices = (await DeviceInformation.FindAllAsync(str)).ToArray();
    return await HidDevice.FromIdAsync(devices[0].Id, FileAccessMode.Read);
}

こんなの書いて、ダイアルを回したり押したりするとこんなダンプがデバッグ出力で得られます。

D2: 01 02 02 00 0A 0B 0C 0D 3A
D2: 01 02 03 00 0A 0B 0C 0D 3A
D2: 01 02 02 00 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FE FF 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FC FF 0A 0B 0C 0D 3A
D2: 01 02 FB FF 0A 0B 0C 0D 3A

解析結果

このバイナリの中身をよく見てるとだいたいこんな構造みたい。

struct Report{
  byte One;
  byte Flags;
  sbyte Degree;
  byte Orientation;
  byte[6] Nazo;
}

2バイト目のFlagsとして名前を付けてみたところは、ビットフィールドになっているようでそれぞれ次の意味っぽい。

ビット 意味
1ビット目 押し下げ状態。1で押してる状態
2ビット目 回転中?

3バイト目は符号付の値のようで、右回転で正の値、左回転で負の値になってる感じ。WinRTのデフォルトより元気で1度ごとにレポートがきます。

4バイト目は回転方向で、右回転なら00、左回転ならFFが入っています。

で、のこった6バイトはよくわからん。

まとめ

SOUND VOLTEXごっこできそうですね!

Surface Dialを2個接続するとどーなるの?

雑記

Q. Surface Dialって2個接続するとどーなるの?

BLEなHIDデバイスなので普通に接続できるはずだけどいったいどうなるの…?

A. APIからは1個に見える

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (_, e) => {
  Debug.WriteLine(e.RotationDeltaInDegrees );
};

こうした時に、1つ目を回しても、2つ目を回してもそれぞれの回転量に応じた値がここでよばれる。 残念ながら別々のダイアルとして扱うことはできないみたい。

ちなみに、デバイスマネージャーからはこうみえてます。 f:id:tmyt:20161127132805p:plain

なのでいちおうAPIより下では別々のものにみえてました。

C++/Win32からSurface Dialをさわってみたよ

雑記

TL;DR

  • C++/Win32からRadialControllerなんて触るもんじゃない
  • ABI::Windows::Foundation::Collectionsに対する実装はSDKに含まれない
  • MIDLコンパイラ/ns_prefixが必須

AzureaでSurface Dialを扱えるプラグインを書きました

Surface Studioと同時に発表されたSurface Dial、回せて、クリックできるだけのデバイスなのです。 これがMicrosoft Global Summitへ参加したタイミングでちょうど発売されて運よく入手することができたので、 UWP*1も、WPF*2も、やってる人がすでにいるので面白そうなのでC++から触ってみました。

ただただ触ってみるだけじゃ面白くないので、せっかくだから2010年にWindows Mobile 5/6向けにリリースしたTwitterクライアントでSurface Dialが使えるようになるプラグインを書きました。

Surface Dialを制御するCOMオブジェクトを実装して、さらにそれをJScriptから制御してるって感じです。

gist.github.com

デスクトップアプリからSurface Dialを操作する

Surface DialをはじめとするRadialControllerははWinRT経由でOSに統合されているので、UWPからならとても簡単に扱えます。たとえばこんな感じに。

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (sender, e) => { }
controller.ButtonClicked += (sender, e) => { }

このAPIですが、シグネチャを見てわかるように、UWPのViewを必要としています。 しかし、デスクトップアプリからだとViewが存在しないのでウィンドウハンドルを使ってインスタンスを生成できるなにがしが必要になってきます。 といっても、.NETからならそれほど大変なものでもなく、COMオブジェクトを経由してインスタンスを生成することができます。*3

[System.Runtime.InteropServices.Guid("1B0535C9-57AD-45C1-9D79-AD5C34360513")]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
interface IRadialControllerInterop
{
    RadialController CreateForWindow(
    IntPtr hwnd,
    [System.Runtime.InteropServices.In]ref Guid riid);
}

これでインスタンスを取得すればだいたいどうにかなります。ですが、なかなかそれなりにつらいよ。ってことが以下で共有されています。

grabacr.net

ところがどっこい、C++だともう少したくさんめんどい。まず、ヘッダファイルとかはWindows SDK 14393に含まれています。 デスクトップC#だと自前定義していたIRadialControllerInteropもSDKで定義があります。なので、インスタンスの取得はこういう感じ。

// IRadialControllerIntropを取得して
Microsoft::WRL::ComPtr<ABI::Windows::UI::Input:IRadialControllerInterop> controllerIntrop;
Windows::Foundation::GetActivationFactory(
    HStringReference(RuntimeClass_Windows_UI_Input_RadialController).Get(),
    &controllerInterop);

// HWNDに対するIRadialControllerを取得する
Microsoft::WRL::ComPtr<ABI::Windows::UI::Input::IRadialController> controller;
controllerInterop->CreateForWindow(hwnd, IID_PPV_ARGS(&controller));

イベントハンドラの追加がめんどくさめなことを除けば、だいたい同じ感じでどうにかなります。

システム定義のアイコンを消したい

Surface Dialを長押ししたときにでるメニューは、デフォルトだとシステム定義のアイコンがいくつかならんでます。

https://i-msdn.sec.s-msft.com/en-us/windows/uwp/input-and-devices/images/windows-wheel/surface-dial-menu-offscreen.png

これを消したりするには、RadialControllerConfiguration::SetDefaultMenuItemsを呼び出すことでできます。 ちなみにMSDNに特に書いてないですが、カスタムアイコンを追加せずにシステムアイコンをすべて消そうとすると、呼び出しは失敗して システム設定された状態にリセットされます。どうやら仕様だそうです。

SetDefaultMenuItemsは、.NETの世界からだとIEnumerable<RadialControllerSystemMenuItemKind>を引数として取るので、 new List<RadialControllerSystemMenuItemKind>();とか適当にやればインスタンスが作れるのですが、C++だと微妙に話が違ってきます。

C++での引数はABI::Windows::Foundation::Collections::IVector<ABI::Windows::UI::Input::RadialControllerSystemMenuItemKind>*となっています。 ちなみに似たようなの3種類あります。

  1. ABI::Windows::Foundation::Collections::IVector
  2. Windows::Foundation::Collections::IVector
  3. Platform::Collections::Vector

似たようなのがいろいろあってめんどくさいですが、1はWinRT ABIでやり取りされる型のインターフェース。2はWinRT内部でやり取りされる型のインターフェース。3は2に対する実装。です。 2が指示されている場合は、3をインスタンス化して使えばOKですが、1が指示されているばあいは、対応する型を手書きする必要があるようです。*4

今回は1が指示されているので、適当に実装するなりなんなりして使えばおっけーです。

MIDLを要求される

ABI::Windows::Foundation::Collections::IVector<T> を実装したクラスを作った。Microsoft::WRL::Make<T>経由でインスタンスも作った。 これで完璧。とおもったらそうでもなく。ABI::Windows::Foundation::Collections配下のインターフェースに対応する実装はMIDLでの定義が必要です。 プロジェクトにIDLファイルを追加して、次のようなコードが必要です。

import "windows.foundation.idl";
import "windows.idl";

namespace Dokoka
{
  declare {
    interface Windows::Foundation::Collections::IVector<Windows::UI::Input::RadialControllerSystemMenuItemKind>;
  }
}

これをビルドすると、たぶん失敗します。プロジェクトを右クリックして、プロパティを開いて、MIDLの設定を少し変えます。

  • General -> Additional Metadata Directories
    • C:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0*5
  • Advanced -> Prepend with 'ABI' namespace
    • Yes

これでビルドすると、IDLファイルのファイル名に _h.h を追加したファイルが生成されます。 このヘッダをincludeして、ABI::Windows::Foundation::Collections::IVector<T>の実装をMicrosoft::WRL::Make<T>で作ってあげれば やっとABI空間のIVector実装のインスタンスが手に入るわけです。

このあたりをやらないと

midlrt : error MIDL4034: Failed to load a dependency file. Windows.winmd (HRESULT:0x80070002 - The system cannot find the file specified. )

と言われたり、

error C2338: This interface instance has not been specialized by MIDL. This may be caused by forgetting a '*' pointer on an interface type, by omitting a necessary 'declare' clause in your idl file, by forgetting to include one of the necessary MIDL generated headers.

と言われたりします。

それから、Prepend with 'ABI' namespaceを付けてないと、MIDLコンパイラが出力したヘッダファイルのコンパイルに失敗します。

おわりに

C++/Win32からSurface Dialを使うのは、IVectorさえ出会わなければそんなに大変じゃないです。IVectorもわかってしまえば大したことないです。 私のぐぐり力が低いばかりに、解決まで3日もかかってしまったのが非常にあれですが…

カスタムアイコンを追加するのはまだ試してないですが、たぶんWPFのつらいあれよりさらにつらそうだな…とおもいつつ、後日試してみます。

*1:http://blog.okazuki.jp/entry/2016/11/11/171706

*2:http://grabacr.net/archives/7141

*3:Windows classic samplesから引用 https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/RadialController/cs/winforms/RadialControllerInterop.cs

*4:Win2Dも手書きしてたのでたぶんそういうもんらしい

*5:たぶんもっとちゃんとした指定があるはずです…

SylphyHornのIssue #19の件

雑記

これはメモです。

TL;DR

  • SylphyHornというよりOpen.WinKeyboadHookというライブラリの問題
  • ToUnicodeExがKernel-mode Keyboard Bufferを変更するというSideEffectがある

問題

github.com

要約すると、Alt+Right押してAltを離したタイミングで♠が入力されるんだよね…というなぞ現象。

とりあえず調べてみたところ

この現象は、"Unicode対応ではないプログラムの言語"のロケールがEN-USの時に発生している。JA-JPの場合だと、Alt押しながらRight,RightのあとAlt離すで'B'が入力されるので同様の現象を確認可能。 すべての入力可能コントロールで起きるわけではなく、R#を有効にしたVisual Studioや、Explorer、Internet Explorerのアドレスバーで発生を確認。

Spy++でみてみると、SylphyHornを起動していない場合

  • WM_SYSKEYDOWN
  • WM_SYSKEYDOWN
  • WM_SYSKEYUP
  • WM_KEYUP

とメッセージが流れてくることに対して、SylphyHornを起動してると

  • WM_SYSKEYDOWN
  • WM_SYSKEYDOWN
  • WM_SYSKEYUP
  • WM_KEYUP
  • WM_CHAR

と謎のWM_CHARが出現。いったいこれはなんぞや。

原因はOpen.WinKeyboadHookにあるのでは

SylphyHornがキーフックする際に、Open.WinKeyboadHookというライブラリを使ってるのでこっちが原因じゃないの?って調べてみると、このライブラリの中のToUnicodeという関数のどこかがトリガーとなってWM_CHARが生成されてしまっているみたい。

もっと調べてみるとToUnicodeExというWin32 APIを呼び出したことがトリガーとなっていることが分かった。そこでMSDN(英語)を読んでみると、下のほうにちょろっとだけ書いてあるこれ。

As ToUnicodeEx translates the virtual-key code, it also changes the state of the kernel-mode keyboard buffer. This state-change affects dead keys, ligatures, alt+numpad key entry, and so on. It might also cause undesired side-effects if used in conjunction with TranslateMessage (which also changes the state of the kernel-mode keyboard buffer).

ToUnicodeEx function (Windows)

しれっとびっくり大事件。SideEffectとかやめてよ、これいったいどうすればいいの。ということで今日はここまで。あくまでもメモです。

Twitterのレスポンスは人が理解できるものではなくなったのだよ

雑記

TwitterのRESTとStreamingのレスポンスがあまりにも難しすぎて忘れてしまうのでメモ。

  1. RESTでtweet_mode=extendedがついていない場合
    1. 添付画像のURLを含めて140文字以下の場合
      • 今まで通りtext, entities, extended_entities が設定される
    2. 添付画像のURLを含めて140文字を超える場合
      • textには全文表示URLを含んだものが設定される。
      • truncatedフィールドがtrueになる
      • entities, extended_entitiesに画像などは含まれず、全文表示URLのみが含まれる
  2. RESTでtweet_mode=extendedがついている場合
    1. 添付画像のURLを含めて140文字以下の場合
      • textはなくなる。代わりにfull_textが増える
      • truncatedフィールドはfalseになる
      • display_text_rangeフィールドが増えて、本文範囲が明示される
      • entities, extended_entities はこれまで通り
        • 含まれるindicesはfull_textに対して正しい値が格納される
  3. Streamingの場合
    1. 添付画像のURLを含めて140文字以下の場合
      • 今まで通りtext, entities, extended_entities が設定される
    2. 添付画像のURLを含めて140文字を超える場合
      • textには全文表示URLが含まれる
      • entities, extended_entitiesに画像は含まれず、全文表示URLのみが含まれる
      • extended_tweetフィールドが増える
        • ここにfull_text, display_text_range, entities(画像入ってる)がある
        • extended_tweet.entitiesはextended_entitiesと同様に複数枚画像に対応してる

つまるところ、全部のパターンをパースしようと思うと

  1. extended_tweet フィールドを見る
  2. full_text をみる
  3. text をみる

とかしないといけなくてくそめんどい

追記: entities を見るときは

  1. extended_tweet.entities をみる (extended_tweet.entitiesはextended_entitiesを包含しているので単体でOK)
  2. 画像はextended_entities、それ以外はentitiesをみる

で、いけます。こっちもめんどい。

結論

くそめんどい

Windows 10からAndroidのテザリングをONにしてみたい

雑記

追記(10/16 11:02): もう少しちゃんと書きました。

TL;DR

背景

Windows 10 Mobileだと、Bluetoothでペアリング済みでPC、電話ともにBluetoothがONになっているとWifiテザリングの設定を端末に触ることなくONにできる機能がありますよね。 macOSiOSでもできるあれです。あれのWindows版。VAIOXperiaのセットでも独自実装されてるやつがありますね。

でも今日はWindows 10の標準実装でMicrosoft拡張のMS-TCCです。詳しいことがEngadgetに書いてました。

japanese.engadget.com

ようするにBluetoothでMS-TCCというプロファイルをサポートしていれば、Windows 10Wifi設定からテザリングを有効にできますよ。という感じ。

MADOSMAをこの機能のためだけに比較的使ってたのですが、よく考えたらこれAndroidでもできるんじゃないかなぁ…と思い、調べてたらこの仕様ってちゃんとドキュメントあるんですね*1

下調べ

MS-TCCのドキュメントを読むとやらないといけないことと、必要な情報がいくつか。

  • 必要なもの
  • やらないといけないこと
    • SDPでGUID {232E51D8-91FF-4c24-AC0F-9EE055DA30A5} を広告
    • リクエストを処理してテザリングを有効にする
  • やったほうがいいこと
    • 端末が圏外かどうかとか見てエラー返す

必要なもののうち、ESSIDとパスフレーズWifiManager#getWifiApConfigurationをリフレクションで叩くと普通に読める。問題はBSSIDで、フィールドはあるけどここに入ってない。BSSIDは通常MACアドレスと同じなのでMACアドレスを読めばいい…のだけど、Android 6.0からWifiMACアドレスが読めなくなってしまった。ので今回はなぜかまだ正しい値を読めるip addrコマンドを実行して結果からMACアドレスを探すことに。

やらないといけないことについては、BluetoothAdapter#listenUsingRfcommWithServiceRecordBluetoothソケットを作るとSDPは広告できるし、リクエストを処理してテザリングを有効にするのはWifiManager#setWifiApEnabledをこれまたリフレクションで叩くと実現できそう。

できました

そんなこんなでやってみたら動いたのでGitHubにソースとかおいておきました。

github.com

ところで、MS-TCCのドキュメントを読んでいただければわかるとおもいますが、この仕様、テザリングをONにすることはできるのにOFFにすることはできないんですよ。なのでテザリングの自動OFF機能がついてる機種以外はONになりっぱなし。最後OFFだけ手動でやってください。

いまのところAPKなしでソースだけなので各自ビルドしてください。あとBluetoothがOFFだった時のエラー処理が何も入ってないのでたぶんやばいです。 BluetoothがOffだったりOn/Offされたときにおかしくならないようにして、APKにしました。GitHubのReleasesからどうぞ*2

これできたからRobinのテザリングWindows 10からリモートでONにできるようになったのでますますMADOSMAの出番が減ってしまいそうだなーって…

ドメインレジストリが逮捕されてしまったら

雑記

追記 (8:39 AM): よく読んだらレジストリでした。

背景

僕はKA.TJというドメインを101domainで購入し、優先度の低いメールアドレスのために運用していました。 このドメインは、今年の4月が有効期限となっていたので2月に更新手続きをしていました。

ところが、いつまでたってもPending Approbal(保留中)という状態から一向に進みません。 サポートチケットを何回か作って、どうなってるんだ、レジストリをせっついてくれ。と言っていたのですが、まったく状況は改善しません。 まぁそんなに急いでなかったので割と放置してたというのもあるんですが、ぶっちゃけ放置してました。

ちなみにTJドメインタジキスタンドメインです。タジキスタンは2016年10月11日現在の外務省海外安全情報によると”レベル2: 不要不急な渡航は止めてください”ですね。

事件

そういやあれ、いまだにPendingだしいい加減払い戻ししてもらいたいなぁ…と最近ふと思い出し、そのうちサポートにまた問い合わせようかな。 と思っていたところ、たまたま今朝チャットサポートが使える時間に思い出したのでチャットサポートに問い合わせてみることに。

またレジストリせっついとくって言われるだけかなー。と思いきやここで驚きの返答が。

22:14 Corey K: The TJ registry has been unavailable to the world. Apparently the people running the registry were arrested.
22:15 Yutaka: !? It is really?
22:15 Corey K: Apparently
22:17 Corey K: I recommend you open a support ticket, our account manager that handles .TJ will respond

ざっくりこういうかんじ。

僕「なーなーなんでいまだにPendingなん
サポート「TJレジストリが世界的に利用できなくなってる。たぶんだけどレジストラを運用してた人間が逮捕されたっぽい
僕「まじすか
サポート「まじっぽい

学び