2015年8月18日火曜日

バッファローのルーターをC#から再起動させる

さて、今回もニッチなテーマになりました。

バッファローのルーターは比較的安定しており、バグも少なく使いやすいです。VPN機能を内蔵しているのものも比較的安価に販売されているので、非常に重宝しています。

しかし、私の使っているWZR-600DHP2というモデルですが、よくわかりませんが、たま~にVPN接続を解除した時にPPPoE接続が切断され以後再起動をかけるまでインターネットにつながらないという状況が起こってしまうようです。そうすると、VPNもつながりませんし、そもそもメール通知やTwitter通知を仕込んでるソフトも機能しなくなるので外から見ていて自宅のPC群が生きているのか全く分からない状態になってしまいます。

WZR-600DHP2には定期的に自身を再起動させる機能を持っていますが、別に不調じゃないのに再起動させる意味もありませんし、ここは、宅鯖から再起動を自動的にかける方法を探ってみました。

さて、まずはどうやったら再起動をかけられるか見当をつけてみます。
ブラウザからルーターにアクセスし、再起動画面を出してみます。そうすると、再起動画面のフレームのURLが
http://192.168.***.1/cgi-bin/cgi?req=tfr&id=55

であることがわかります。(IPアドレスは適当に各自の環境に置き換えて考えてください)
さらに、そのソースを見てみると、再起動ボタンを押すと
http://192.168.***.1/cgi-bin/cgi?req=inp&res=waiting_page.html
に飛ぶことがわかります。
よっしゃ、ここのURLを開けば再起動がかけられる!


\デーン/
URLを直にたたいただけではだめでした…。

実は、バッファローのルーターではこのような不正(っぽい)アクセスを防止するために、POSTパラメーターに適当な値を付けてURLリクエストをかけなければいけないようです。

というわけで、じっくり再起動周りのソースを調べてみると、inputタグは「submit=再起動」以外にも隠しパラメーターとしてsWebSessionnumとsWebSessionidがあることがわかると思います。この番号もPOSTパラメーターとして送信してあげないと再起動ができないわけですね。

というわけで、流れが見えてきました。
  1. http://192.168.***.1/cgi-bin/cgi?req=tfr&id=55にアクセスし、sWebSessionnumとsWebSessionidの値をパースする
  2.  http://192.168.***.1/cgi-bin/cgi?req=inp&res=waiting_page.htmlを開くときのPOSTパラメーターに、「submit=再起動」のほか、sWebSessionnumとsWebSessionidも入れてあげる
これで、プログラムから再起動ができそうです。


というわけで、さっそくWebClientを使ってhttp://192.168.***.1/cgi-bin/cgi?req=tfr&id=55のHTMLをダウンロードしようとしたところ、例外が発生しました。
System.Net.WebException
サーバーによってプロトコル違反が発生しました. Section=ResponseStatusLine
これはどうもお行儀が悪いウェブサーバーにアクセスしたときに起こる例外みたいで、.NETが厳密にHTMLプロトコルを検証した結果、違反があったときに発生するみたいです。これを回避するにはApp.config内でUseUnsafeHeaderParsingプロパティをTrueにしてあげればいいみたいです。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
    <system.net>
        <settings>
            <httpWebRequest useUnsafeHeaderParsing="true" />
        </settings>
    </system.net>
</configuration>


さて、これで準備は整いました。実装していきましょう。

ちなみにですが、バッファローのルーターでは、ログインの認証にBASIC認証を用いています。BASIC認証は、WebClient.CredentialsプロパティにIDとパスワードを入れたうえで各種リクエストを送れば大丈夫です。

static Regex reg = new Regex(@"<input type=hidden name=sWebSessionnum value=(?<num>\d+)><input type=hidden name=sWebSessionid value=(?<id>-?\d+)>", RegexOptions.Compiled);
static Encoding encoding = Encoding.GetEncoding("EUC-JP");

public static async Task RebootRouter(string RouterUrl, string Password)
{
    await RebootRouter(new Uri(RouterUrl), Password);
}

public static async Task RebootRouter(Uri RouterUrl, string Password)
{
    const string user = "admin";

    using(WebClient wc = new WebClient()) {
        wc.Credentials = new NetworkCredential(user, Password);
        wc.Encoding = encoding;

        string init = await wc.DownloadStringTaskAsync(new Uri(RouterUrl, @"/cgi-bin/cgi?req=frm&frm=init.html"));

        Match m = reg.Match(init);
        if(m.Success) {
            string num = m.Groups["num"].Value;
            string id = m.Groups["id"].Value;

            string post = $"sWebSessionnum={ num }&sWebSessionid={ id }&reboot={ HttpUtility.UrlEncode("再起動", encoding) }";
            wc.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
            string resText = await wc.UploadStringTaskAsync(new Uri(RouterUrl, @"/cgi-bin/cgi?req=inp&res=waiting_page.html"), post);

            if(!resText.Contains("再起動中です。"))
                new InvalidOperationException(resText);
        } else
            new FormatException("cannot parse sWebSessionnum/sWebSessionid");
    }
}

さて、こんな感じでどうでしょうかね。
結構適当な実装です。HTMLもまじめにパースせず、スペースの取り方とかは決め打ちで正規表現でsWebSessionnumとsWebSessionidを抽出しています。そして、抽出に成功したらPOSTパラメーターを作り、UploadStringTaskAsyncメソッドでそれを送信しています。

なお、バッファローのルーターのWebサーバーの文字コードはEUC-JPになっています。Encodingを指定しないと日本語で文字化けするので気を付けてください。

余談ですが、C#6.0の新機能をここでは使っています。$"~"の構文ですね。Visual Studio 2015じゃないとコンパイル通らないので、古い環境を使っている人がいたら適宜書き換えてください。

try {
    RebootRouter(@"http://192.168.***.1/", "****").Wait();
    Console.WriteLine("Router Reboot has successfully started.");
}
catch(AggregateException e) when (e.InnerExceptions.Any(p => p is InvalidOperationException || p is FormatException || p is WebException)) {
    foreach(var ie in e.InnerExceptions) {
        Console.WriteLine(ie.GetType());
        Console.WriteLine(ie.Message);
    }
}

このメソッドの呼び出しはこんな感じでいいですかね。1つ目の引数にルーターのURL、2つ目にパスワードです。非同期メソッドになっているので、例外が起きた時はAggregateExceptionになることに注意してください。ここもC#6.0になってから簡単に例外を振り分けられるようになりました。

このプログラムを実行して2分くらい待つと再起動の完了です。
めでたしめでたし。

2015年8月7日金曜日

LINQ to Twitter ver.3.1.2のUserStream

C#からTwitterするならLINQ to Twitterですが、今まで、これには不満点が1つありました。

それは、UserStreamの実装がだいぶ雑だったことです。

UserStreamはそもそもツイートだけでなく、例えばふぁぼ通知、フォロー通知、ツイ消し通知など、様々な情報が流れてきます。そのため、UserStreamは各種Rest APIのJSONとは違っていて、それこそ多種多様なデータが流れてきていました。そして、その多種多様なデータを適当に振り分けてくれるような機能が以前のLINQ to Twitterにはありませんでした。

なので、UserStreamの処理を書くときに限って、自前でJSONの処理をしなければいけませんでした。せっかくの.NET向けライブラリなのにとても残念なことになっていたわけですね。

無論、流れてくるツイートのみをパースしたければ、StatusクラスのコンストラクタにJsonData型を引き渡すことができるオーバーロードがありますから、それにぶち込んで、もしも例外等が吐かれたらツイート以外のデータだったんだなということで無視する、なんて方法はありました。私も以前からその方法をよく使っていましたし、それで事足りることもしばしばありました。

もしくは、UserStreamsParser for LinqToTwitterのような別のライブラリと組み合わせてパースさせることもできましたが、しばしば仕様変更で使えなくなってしまったり、いろいろと不都合なこともありました。

さて、ここまで勿体ぶったからには何が言いたいか見当が付いてきたかと思いますが、ついにLINQ to TwitterのほうでこのUserStreamのパース機能が実装されました。 (´◔౪◔)۶ヨッシャ!

現在のLINQ to Twitterの最新バージョンはver.3.1.2ですが、どうも3.1.0あたりから実装されたっぽいです(未検証)。

というわけで、さっそく使ってみました。

SingleUserAuthorizer auth = new SingleUserAuthorizer()
{
    CredentialStore = new SingleUserInMemoryCredentialStore()
    {
        ConsumerKey = "****",
        ConsumerSecret = "****",
        AccessToken = "****",
        AccessTokenSecret = "****"
    }
};

TwitterContext context = new TwitterContext(auth);

(from p in context.Streaming where p.Type == StreamingType.User select p).StartAsync(async s => {
    await Task.Run(() => {
        switch(s.EntityType) {
            case StreamEntityType.Unknown:    //Unknownはスルー
                break;
            case StreamEntityType.Status:    //ツイートが流れてきた
                ShowStatus((Status)s.Entity);
                Console.WriteLine();
                break;
        }                    
    });
});


こんな感じになります。
UserStreamで流れてくるStreamContentクラスにEntityTypeとEntityという2つのプロパティが追加されました。EntityTypeはStreamEntityType列挙体、Entityはobject型になっています。

例えば、ツイートが流れてくるときは、EntityTypeがStreamEntityType.Statusになっていて、EntityにStatus型のオブジェクトが入っています。なので、適宜キャストしてあげれば流れてきたツイートを適当に処理することができます。

ほかにも、例えばふぁぼられたときはStreamEntityType.Eventが流れてきます。この時のEntityはEventクラスのオブジェクトで、EventNameに"favorite"、SourceとTargetにそれぞれふぁぼった人とふぁぼられた人のUserデータが入っています。
ふぁぼられたツイートはTargetObjectだと思われますが、実際には"JsonData object"というテキストが入っているだけで、ふぁぼられたツイートはわかりません。これはおそらくLINQ to Twitterのバグでしょう。しっかり検証していませんが、おそらくフォロー通知などもEntityがEventとして流れてくると考えられるので、そのあたりで実装が不十分になっているのでしょう。これは是非とも今後のアップデートに期待したいですね。

ほかにもDeleteやDirectMessageなど、様々なUserStreamの情報に対応しているようですから、いろいろいじってみると面白いかもしれません。

 ちなみに、RetweetedみたいなEntityが無いなと思っていたら、どうもこれ、普通にStatusと一緒にやってくるみたいですね。要するに「リツイートされたツイートの投稿主が自分だったらリツイートされたってことでしょ 」という体みたいです。まあそれくらいの実装なら全然難しくはなさそうですよね。ハマらないように、念のため。

2015年8月4日火曜日

Janetterで自分あて@無しリプライをする

※今回はプラグインではなくJanetterの改造となります。 より慎重にお願いします。

さて、Janetterの登録アカウント数がTwitterの定める上限に達してから久しいですが、最近Twitter公式への複数画像の投稿機能、DMの文字数制限撤廃対応など最低限のメンテナンスはしてくれています。とてもありがたいです。

しかし1つ、Twitterの機能に追従していないことがあります。それが「自分あて@無しリプライ」です。Twitterは基本的に対応するユーザーの「@screen_name」が本文に入っていないとInReplyToが付きません。一昔前までは自分あてリプライでもそうだったと記憶していますが、最近(と言っても2年以上前ですが)、自分あてリプライに限ってその機能が撤廃されました。
それによって、140字で収まらないような長文ツイートをするときに、InReplyToを付けることによってよりわかりやすく一連のツイートを関連付けることができます。

さて、どうやるのかですが、これはJanetterの改造になります。ですが、とてもシンプルです。
Janetter4.3.1.0の場合ですが、

C:\Program Files (x86)\Janetter2\Theme\Common\js\janet\tweetedit.js

の540、541行目が該当するコードになっております。
if(self._replayId>0 && textarea.val().indexOf('@' + self._replayTo)==-1)
    self.removeReplyTo();

InReplyToが付いていて、かつそのscreen_nameがテキストボックスに含まれてなければReplyToを削除するようにできています。

ここに条件を加えてあげましょう。「さらに、そのscreen_nameが自分のアカウント名じゃなかったら」を追加することで想定した動作になります。
if(self._replayId>0 && textarea.val().indexOf('@' + self._replayTo)==-1 && self._replayTo.toLowerCase()!=self._account.screen_name.toLowerCase())
    self.removeReplyTo();

これだけで完成です。自分あてリプライのときに限って、InReplyToが削除されなくなりました。

おそらくこのファイルはJanetterの更新で入れ替えられますから(このファイル内でDM字数制限撤廃機能が実装されていますし)、そのたびに修正する必要が出てくるかと思います。 その場合、もちろん何行目かも変わってくる可能性がありますが、まあその辺は臨機応変に。

2015年7月4日土曜日

Windowsの「追加の時計」をいじる

さて、Windowsには追加の時計というものがありますね。


このように、タスクバーの右下の時計を押したら出てくるやつに世界時計を2つまで追加できる機能です。家族、職場の知り合い等が海外に出かけているとき、これを設定しておくとその人が今何時なのかが一目でわかるので、電話を掛けるタイミングが見当付くようになるなど、とても便利なものです。

しかしこれ、設定のUIがとてつもなく不便です。


このように、一覧から表示するタイムゾーンを選択しなければなりません。しかも、表示されるのは国名ではなく、都市名、もしくは地域名です。そして、このリストにはUTCとの時差が同じ都市がいくつもありますが、これの選択を誤るとサマータイムの反映がうまくされず、実際とは異なる時刻が表示されることがあります。
時刻を表示したい地域について、周辺都市やタイムゾーンを熟知している人なら簡単に選択できるでしょうが、それができるのは多分その国に住んでいる人か、もしくはよほどの地理オタクくらいなものです。

すなわち、普通に使うには、これはとても不便なのです。

これをもうちょっと、例えば地図で選択した地点から直接タイムゾーンを判定して表示したり、 国名を入力したら自動的に選択してくれたり、はたまたTwitterの位置情報からタイムゾーンを入力したりするようなUIが必要ですよね。
そういった設定ができるようなソフトウェア開発について考えていきましょう。

「追加の時計」の設定を変更する

さて、それではどうやってこの時計を設定すればいいでしょう。
たいてい、こういうのはレジストリに書き込まれていますから、レジストリでそれっぽいキーを探します。
そうすると、見事に出てきました。

HKEY_CURRENT_USER\Control Panel\TimeDate\AdditionalClocks

ここに「1」というキーと「2」というキーがあり、これがそれぞれ追加の時計1,2に対応しています。


値は非常にシンプルです。
DisplayNameは表示名ですね。時計のタイトルで、任意の文字列です。
Enableはその時計が有効かどうかです。0で無効、1で有効ですね。
TzRegKeyNameがタイムゾーンを表すキー名です。なんのこっちゃってなるかもしれませんが、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zonesに格納されている各タイムゾーンに対応するレジストリキー名となります。

さて、となると、このタイムゾーンのレジストリキーまでパースしてやらなきゃいけないのかという話になりそうですが、そんなことはありません。.NETのTimeZoneInfoクラスIdプロパティの値が、このレジストリキーに対応しています。TimeZoneInfoはGetSystemTimeZonesメソッドによって一覧を取得できるので、これを適当に選択してレジストリに書き込んであげれば時計は設定できるわけですね。

というわけで、追加の時計を変更するメソッドは下記のような形になります。

/// <summary>
/// 「追加の時計」に時刻を設定するメソッド
/// </summary>
/// <param name="ClockNumber">時計の番号。1または2</param>
/// <param name="DisplayName">表示名</param>
/// <param name="IsEnable">有効にするか、無効にするか。</param>
/// <param name="TimeZone">設定するタイムゾーン</param>
public static void SetAdditionalClock(int ClockNumber, string DisplayName, bool IsEnable, TimeZoneInfo TimeZone)
{
    if(ClockNumber < 1 || ClockNumber > 2)
        throw new ArgumentOutOfRangeException("ClockNumber");
    if(IsEnable) {
        if(DisplayName == null)
            throw new ArgumentNullException("DisplayName");
        if(TimeZone == null)
            throw new ArgumentNullException("TimeZone");
    }

    using(RegistryKey Key = Registry.CurrentUser.CreateSubKey(string.Format(@"Control Panel\TimeDate\AdditionalClocks\{0}", ClockNumber))) {
        if(IsEnable) {
            Key.SetValue("DisplayName", DisplayName, RegistryValueKind.String);
            Key.SetValue("Enable", 1, RegistryValueKind.DWord);
            Key.SetValue("TzRegKeyName", TimeZone.Id, RegistryValueKind.String);
        } else
            Key.SetValue("Enable", 0, RegistryValueKind.DWord);
    }
}

今まで追加の時計を設定したことが無い環境ではレジストリキーすら存在しないようなので、そのような環境のためにCreateSubKeyにてキーを作成(もしくは存在する場合はそれを開く)しています。
あとは上記の3つのパラメーターを書き込んであげているだけなので至ってシンプルです。これで追加の時計の設定ができてしまいます。驚くほどあっさりしていました。

緯度経度からタイムゾーンを取得する

さて、追加の時計の設定変更は簡単にできましたが、そもそもTimeZoneInfo.GetSystemTimeZonesが先ほどのWindows標準のタイムゾーン一覧を表示するコンボボックスの内容を返すだけなので、どうにかして何かしら別の情報(緯度経度など)をタイムゾーンに変換する必要があります。

まあ、もっともお手頃そうなのはGoogle Time Zone APIですかね。
時刻と緯度経度を指定すると、JSONでその夏時刻を含めたタイムゾーンを返してくれます。なので、いつも通りJson.NETを使ってタイムゾーンを取得するクラスを作りました。

public class GoogleTimeZone
{
    private GoogleTimeZone(InternalGoogleTimeZone Source)
    {
        DaylightSavingTimeOffset = TimeSpan.FromSeconds(Source.dstOffset);
        RawOffset = TimeSpan.FromSeconds(Source.rawOffset);
        Status = Source.status;
        TimeZoneId = Source.timeZoneId;
        TimeZoneName = Source.timeZoneName;
    }

    /// <summary>
    /// サマータイムのオフセット
    /// </summary>
    public TimeSpan DaylightSavingTimeOffset { get; private set; }

    /// <summary>
    /// サマータイム関係なしのオフセット
    /// </summary>
    public TimeSpan RawOffset { get; private set; }

    /// <summary>
    /// ステータス
    /// </summary>
    public string Status { get; private set; }

    /// <summary>
    /// タイムゾーンのID
    /// </summary>
    public string TimeZoneId { get; private set; }

    /// <summary>
    /// タイムゾーン名
    /// </summary>
    public string TimeZoneName { get; private set; }

    public override string ToString()
    {
        if(Status != "OK")
            return Status;
        else {
            string RawOffsetText = (RawOffset < TimeSpan.Zero ? "-" : "+") + RawOffset.ToString("hh\\:mm");
            string DSTText = string.Empty;

            if(DaylightSavingTimeOffset != TimeSpan.Zero) {
                DSTText = (DaylightSavingTimeOffset < TimeSpan.Zero ? "-" : "+") + DaylightSavingTimeOffset.ToString("hh\\:mm");
                DSTText += "(DST)";
            }

            return string.Format("(UTC{0}{1}) {2}/{3}", RawOffsetText, DSTText, TimeZoneName, TimeZoneId);
        }
    }

    public static async Task<GoogleTimeZone> CoordinateToTimeZoneAsync(double Latitude, double Longitude, DateTime? UtcTime = null, string ApiKey = null)
    {
        if(UtcTime == null)
            UtcTime = DateTime.UtcNow;

        long UnixTime = (long)(UtcTime.Value - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds;

        string url = string.Format(@"https://maps.googleapis.com/maps/api/timezone/json?location={0},{1}&timestamp={2}", Latitude, Longitude, UnixTime);

        if(!string.IsNullOrEmpty(ApiKey))
            url += "&key=" + ApiKey;

        string json;

        using(WebClient wc = new WebClient()) {
            json = await wc.DownloadStringTaskAsync(url);
        }
        return new GoogleTimeZone(JsonConvert.DeserializeObject<InternalGoogleTimeZone>(json));
    }
}

internal class InternalGoogleTimeZone
{
    public int dstOffset { get; set; }
    public int rawOffset { get; set; }
    public string status { get; set; }
    public string timeZoneId { get; set; }
    public string timeZoneName { get; set; }
}

Json.NET用に単純なInternalGoogleTimeZoneクラスを作り、そこからプロパティのアクセスレベルや型をしっかり作ったクラスに変換してあげる形にしています。これで、緯度経度をタイムゾーンに変換できるようになりました。
ちなみに、このAPIでは緯度経度のほかに時刻を渡す必要があります。その時刻によって、サマータイムの季節かどうかを判別してくれるようです。

GoogleのタイムゾーンをWindowsのタイムゾーンに変換する

さて、Googleからタイムゾーンが取得できるようになりましたが、Windowsの形式とは若干異なります。UTCからの時差はどちらも出ますが、サマータイムが採用されているかどうかの判別はGoogleのほうには無く、というよりも、タイムゾーンを求める段階で時刻を指定してサマータイム中ならばサマータイム中だという返答が返ってくるだけになります。
なので、この辺を上手く扱って、GoogleのタイムゾーンをWnidowsのタイムゾーンに変換してやらねばなりません。これは結構苦労しました。

 結果から言うと、下記の手順で求めました。
  1. 子午線が一致するタイムゾーンをWindowsのタイムゾーンのリストから抜き出す
  2. 都市名等を単語ごとに分解し、都市名にかかわらないstandard, time, daylight, city, summerなどの文字列を除外する(それで残った単語を『キーワード』と呼ぶことにする)
  3. GoogleタイムゾーンとWindowsタイムゾーンで、一致するキーワードの個数がもっとも多いペアが、そのGoogleタイムゾーンに対応するWindowsのタイムゾーンということにする。キーワードの個数が同じ組み合わせが複数あった場合は、キーワードの個数がもっとも少ないものを採用する。
まあ何を言いたいかというと、Googleのタイムゾーンでは、例えばロサンゼルス周辺のタイムゾーンを取得しようとすると
America/Los_Angeles
Pacific Standard Time
の2種類の文字列を返してきます。一方、Windowsのタイムゾーンでは
Pacific Standard Time
という文字列が返ってきます。
この「Pacific」という文字列の一致から、対応するタイムゾーンを探し出そうというわけです。
Windowsのタイムゾーンにはほかにも
Pacific Standard Time (Mexico)
などがあります(メキシコの太平洋標準時)が、それらはGoogleのタイムゾーン側にMexicoとかそういった単語が無い場合は最も短い名前の都市名と一致を掛けるので、該当しなくなるわけです。

public async Task GetTimeZone()
{
    GoogleTimeZone gtz = await GoogleTimeZone.CoordinateToTimeZoneAsync(Latitude, Longitude);

    var WindowsTimeZones = TimeZoneInfo.GetSystemTimeZones().Where(p => p.BaseUtcOffset == gtz.RawOffset)
        .Select(p => new { Source = p, Keywords = TimeZoneTextToCityKeywords(p.Id) }).ToArray();
    var GoogleTimeZoneTexts = TimeZoneTextToCityKeywords(gtz.TimeZoneId + " " + gtz.TimeZoneName);

    //子午線が一致する都市のうち
    //1. 一致するキーワードが多いもの
    //2. そのうち、キーワードの長さが最も短いもの
    //を抽出する
    TimeZone = WindowsTimeZones.Select(p => new {
        Source = p.Source,
        SameTextCounts = p.Keywords.Concat(GoogleTimeZoneTexts).Count() - p.Keywords.Concat(GoogleTimeZoneTexts).Distinct().Count(),
        SourceTextCounts = p.Keywords.Count()
    }).OrderByDescending(p => p.SameTextCounts).ThenBy(p => p.SourceTextCounts).First().Source;
}

/// <summary>
/// タイムゾーンのテキストからその地域名に係るキーワードを抜き出すメソッド
/// </summary>
/// <param name="TimeZoneName"></param>
/// <returns></returns>
private static string[] TimeZoneTextToCityKeywords(string TimeZoneName)
{
    string[] spacers = { "/", "_" };
    string[] removes = { "(", ")", "standard", "time", "daylight", "city", "summer" };

    TimeZoneName = TimeZoneName.ToLower();

    foreach(string s in spacers)
        TimeZoneName = TimeZoneName.Replace(s, " ");
    foreach(string r in removes)
        TimeZoneName = TimeZoneName.Replace(r, string.Empty);

    return TimeZoneName.Split(' ').Where(p => !string.IsNullOrEmpty(p)).OrderBy(p => p).Distinct().ToArray();
}

実装はこんな感じになっています。
まあ、本当にこれで完全かと聞かれるとちょっと自信は無いですが、少なくとも最初に子午線の一致を確認しているので、誤判定があったところでサマータイムの問題くらいでしょう。
もしも問題が見つかれば個別にちょっと考えていこうとは思っています。

LINQ to Twitterで位置情報を取得する

さて、緯度経度をどこから持ってくるかと言う話ですが、海外に行っていることをTwitterでつぶやいている人がいたので、Twitterから持ってくることにしました。

しかしこれ、意外とドツボです。

最近、Twitter公式アプリとかを見てもらうとわかりますが、位置情報をつぶやくときに「正確な位置を共有」とかいうオプションがあります。従来のTwitterでは緯度経度ベースの正確な位置情報しかつぶやけませんでしたが、個人情報に配慮したのでしょうか、最近はこういうオプションが生まれました。
そのせいで、位置情報にかかわる機能がややこしくなっているんですね。

Twitterが返す生のJSONまでは調べていませんが、LINQ to Twitterではそのあたりが下記のような仕様になっているようです。

従来の(正確な)位置情報の場合

Status.Coordinates.Latitude
Status.Coordinates.Longitude
に緯度経度が入っています。正確な位置情報が共有されていない場合や、そもそも位置情報を付けてつぶやいていない場合は緯度経度がともにゼロになっています。
なお、正確な位置情報が共有されている場合でも、下記の大雑把な位置情報も入っていますので、そちらも併せて利用することができます。

大雑把な位置情報の場合

Status.Coordinatesはnullにはなりませんが、緯度経度がともにゼロになります。
そして、Status.Place.BoundingBox.Coordinatesに緯度経度がいくつか(多分4つ)入ります。Twitterの開発者向けサイトには
A bounding box of coordinates which encloses this place. 
と書いてあるので、その位置情報のエリアを囲う座標が格納されているという意味なのでしょう。
というわけで、正確な位置情報が共有されていない場合は、そのエリアの頂点の各座標の重心(座標の平均値)を現在位置とすることとしました。

ちなみに、このStatus.Placeには地名の情報も入っています。

Status.Place.FullName: 地域名(東京都千代田区など)
Status.Place.Country: 国名(日本など)

なので、このあたりも使えそうですね。


昔のTwitterクライアントなんかではこの大雑把な位置情報には対応していなかったりしますが、特に今回のタイムゾーンを表示する程度の場合では大雑把な位置情報でも問題はないので、積極的にこちら側の情報も使っていくこととしました。



まあ要するに、
  1. Twitterから緯度経度を取ってくる
  2. Google Time Zone APIでその緯度経度に対応したタイムゾーンを調べる 
  3. キーワード比較でWindowsのタイムゾーンに変換する
  4. その設定を時計に転送する
みたいな流れで時計を自動設定できるようなソフトを作ってみました。はい。
できればGoogleマップで表示している場所のタイムゾーンをみたいな機能も付けたかったのですが、何せWPFのWebBrowserコントロールの闇が深すぎて…

Twitter to WorldClock ver.0.1.0
 ※今回は諸事情あってソースコードは配布しておりません。悪しからず。

2015年7月1日水曜日

NTP時計のうるう秒

さて、先ほどうるう秒が実施されました。
我が家でも無事、NTP時計がうるう秒を表示してくれました。


うるう秒に対応していない時計では、うるう秒後に時計が1秒ずれた状態になります。


もちろん、その後の時計合わせでずれた時刻は修正されるので、対応していない時計でも後に正確な時刻を刻むようになるわけですがね。

それでは、NTP時計の様子は動画で撮影していましたので、こちらを御覧ください。


うるう秒がちゃんと表示できていることがわかると思います。また、右上の液晶に示したLeap Indicator (LI)ですが、8:59:60→9:00:00の遷移の時に0に切り替わっていることが分かるかと思います。これは、単にうるう秒が終わったら0にリセットしているだけで、直ちにNTPサーバーに問い合わせているわけではありません。LIの存在をうるう秒の1秒に読み替えていただけですからね。はい。

にしてもこのレンズ、液晶にピント合わせたら7セグがピンボケするんだなあ…。絞りをもうちょいと絞れば良かった…。

2015年6月30日火曜日

うるう秒前日

さて、本日は新幹線で焼身自殺由来の火災が発生したり、箱根が噴火警戒レベルが3に引き上げられたりと個人的に注目度の高いニュースが多いですが、それに加えて、ついにうるう秒前日となりました。

NTP時計のうるう秒対応

私が作ったNTP時計はすでにうるう秒に対応させており、今回は初めてその実証となります。
ワクワクしています。

そして、本日の9時過ぎ、ついにLeap Indicatorが降ってきました。


NTP時計上部に付いた液晶にLIという項目があり、そこに1と表示されているのが分かるかと思います。Leap Indicatorは2bitで、0が警告なし、1がその日の最後(UTC)に1秒追加されるタイプのうるう秒がある、2が1秒減らされるタイプのうるう秒があるという意味になります。すなわち、NTP時計は本日6/30の最後(=日本標準時の7/1 9:00:00)にうるう秒として1秒を挿入する準備ができている状態になっていることを示しています。

さて、このうるう秒に関して、例えばNICT(情報通信研究機構)では8:59:60を見ようというイベントが行われます。

午前8時59分60秒を見てみませんか?(2015年7月1日(水) 開催)

いつもうるう秒関係のニュースなどで出てくるあの時計ですね。
詳細のPDFなんかには「一般の時計ではこの60秒を表示することはできません」などの記述がありますが、私の家のNTP時計では見ることができます(ドヤァ

というわけで、私は朝9時は自宅待機です。NTP時計でこの8:59:60を見届けるつもりです。楽しみだなあ。

2015年5月27日水曜日

東京メトロAPIをC#で使う

お久しぶりです。
ちょっと最近いろいろと忙しくて、なかなかブログを書いている暇がありませんでした。プログラミングはちょくちょくやっていましたが。

ここ最近はまっているのは、表題の通り東京メトロのAPIです。

https://developer.tokyometroapp.jp/

昨年の9月に東京メトロは、駅情報や列車の運行情報、在線情報などを提供するためのAPIを公開しました。この時は、コンテストに参加する目的で利用が可能でしたが、そのコンテストが終わった後、今年4月から特にそういった目的でもなくAPIが使えるようになりました。
上記の東京メトロの開発者向けサイトに行き、必要な情報を入力してアカウントを取得することでAPIにアクセスするためのアクセストークンを入手することができます。

https://developer.tokyometroapp.jp/app

こちらにはコンテストに参加したアプリケーションや、コンテスト終了後に登録されたアプリケーションなど、様々なアプリケーションがあります。
最近流行りなのはやはりスマホアプリということで、コンテストでもスマホアプリに限定して募集されていましたし、その後もそういったアプリケーションの登録が非常に多いように見受けられます。
その中で、こんなアプリケーションがありました。

Tokyometro4J

東京メトロAPIのJavaラッパーです。

ん?じゃあこれは.NETラッパーを作るしかないな?

というわけで、作ってみました。

TokyoMetro.NET ver.1.0.0

ゆうて、やることはそんなに大変じゃないです。
URLにクエリとアクセストークンを入れてアクセスしたら、それに応じた各種情報をJSONで返してくれるだけです。なので、JSONを適宜パースして、適当なクラスに入れてやればいいだけです。JSONのパースもJson.NETに投げちゃいましたから、実質、ラッパークラスの実装とURLの作成がほとんどですかね。
それをやるだけなんですが、まあいろいろなデータがあるので、それぞれにおいてラッパークラスを実装してやるのは結構面倒でした。あと、最近流行りの非同期処理です。これを実現するために、2倍のメソッドの実装が必要でした。○○Asyncっていうメソッドを実装してやらなきゃいけないですからね…。

上記のライブラリ、例によってデモソフトとしてライブラリを使ったフロントエンドも同梱しています。普通にライブラリを使う分には何も問題無いですが、「とりあえずデモソフト立ち上げよう」と思った方、ごめんなさい。デモソフトはダウンロードしてそのままコンパイルするだけじゃ動きません。動くようにするためには然るべき場所にアクセストークンを入力し、コンパイルする必要があります。
アクセストークンは他人に漏らすわけにはいかないですし、.NETに関してはこんなソフトなどを使えば超簡単に逆コンパイルができてしまいます。オープンソースではもちろんのこと、難読化しない限り、なかなかこういうソフトをexeファイルですら公開しにくいんですよね。
難読化ソフトも無料のものではなかなか最新の.NETバージョンに対応していなかったり、そもそも文字列の難読化に対応していなかったりして使い物になりません。有料のは手を出すのがなかなか大変ですよね。

というわけで、自分で何かソフトを開発する方はもちろん、デモソフトを動かしてみたいと思った方でも東京メトロに開発者登録をして使うようにしてください。ごめんなさい。でもご理解の程お願いいたします。


さて、このライブラリは多分全部のAPIを網羅しています。見落としがあったらごめんなさい。
使い方はとても簡単で、

var api = new TokyoMetroApi("Consumer Key");

このようにTokyoMetroApiクラスのコンストラクタにアクセストークンを与えてインスタンス化するだけで準備は完了です。あとは、Search○○DatapointAsync()みたいな名前のメソッドを呼ぶだけで大体のデータは読み出せます。これらのメソッドは条件を指定することができて、例えば

var fukutoshin = (await api.SearchRailwayDatapointAsync(SameAs: IDs.Railway.TokyoMetro.Fukutoshin)).Single();

のように名前付き引数で条件を指定することで、この場合は副都心線のみの路線情報を取得しています。例えばRailwayFareなんかは条件を何も指定しないとあらゆる2駅間の組み合わせ(多分)の運賃が返ってくるので、サーバーにもクライアントにもネットワークにも大きな負荷を掛けることになってしまいます。必要十分なデータのみを読み出すことで、サーバーへの負荷を抑えましょう。
一応、こういった条件指定も少しだけできるので、LINQ to TokyoMetroとか実装できれば面白そうだなーって思ったりもしましたが、そもそもLINQの実装の話はあまり詳しくなかったり、LINQレベルでwhereが詳細に指定でいるわけでもないので、今回は見送りました。

ちなみに、非同期メソッドは末尾にAsyncが付いていて、付いていない場合は同期的に実行されます。ウェブアクセスが絡むメソッドは全部Asyncの有りバージョンと無しバージョンの2種類を実装しているはずですので、目的に合わせて使い分けてください。

それでは、各データのクラスの代表として、駅データを見てみましょう。

public class Station : ResponseBase
{
    internal Station(InternalStation Source) : base(new UCode(Source.Id))
    {
        //省略
    }

    public DateTime Date { get; private set; }

    public ID SameAs { get; private set; }

    public string Title { get; private set; }

    public string Region { get; private set; }

    public ID Operator { get; private set; }

    public ID Railway { get; private set; }

    public ReadOnlyCollection<ID> ConnectingRailway { get; private set; }

    public ID Facility { get; private set; }

    public ReadOnlyCollection<ID> PassengerSurvey { get; private set; }

    public string StationCode { get; private set; }

    public ReadOnlyCollection<UCode> Exit { get; private set; }

    public Coordinate Coordinate { get; private set; }

    public override string ToString()
    {
        return string.Format("{0} ({1})", string.IsNullOrEmpty(Title) ? string.Empty : Title, Railway == null ? string.Empty : Railway.Title);
    }
}

例えば、Stationクラスにはこのように幾つかIDクラスのプロパティが含まれています。これは、例えば"odpt.Railway:TokyoMetro.Ginza"などの文字列で定義されるIDが含まれています。
そっくりそのままこれをSearchRailwayDatapointAsync()メソッドのSameAs引数にセットして呼び出せば路線情報を引っ張り出せます。また、主要なIDはIDsクラスの中に固定データとして入れてありますので、そこから使うと良いでしょう。また、IDクラスはコンストラクタを持っておらず、ID.CreateOrGet()メソッドを通してインスタンス化する必要があります。シングルトン(と言っていいのかわかりませんが)のためですね。

Railwayとかその他のクラスに関してはVSのオブジェクトブラウザーでクラス一覧を見るなり何なりして使ってもらえればいいかと思います。

ちなみに、東京メトロAPIにはエラッタが多数あります。
コンテストに使われていた関係で、終盤にはAPIのエラッタを修正すると逆にアプリに問題を与えてしまう可能性があるので(コンテスト期間中はアプリのアップデートは禁止されていた)、不合理な仕様や単なるスペルミスやデータミスでもエラッタに放り込んで修正をしなかった背景があるようです。
これに関しては、私のライブラリではまあ最終的にはエラッタに相当するデータは修正した上で提供すると言うスタンスで設計しようかと思っておりますが、手を抜いて全部のエラッタを実装していません。と言うよりか、丸ノ内線支線にかかわるエラッタとスペルミス周辺しか修正していなかったと思います。ごめんなさい。
いやいや、アプリのコンテスト期間が過ぎたのでそろそろ東京メトロさんにはエラッタを直していただきたく…

とまあ、このへんがこのライブラリの概要です。
そうして、このライブラリを駆使して駅の緯度経度や列車の位置情報を求めて、リアルタイムに表示させるソフトを作りました。それがデモソフトです。デモソフトを眺めている様子を早回ししたものが下記の動画のようになります。



この日はたまたま14時半頃に東京で震度4の地震があって、緊急地震速報で全列車が停車する様子が見られました。面白いですね。
あ、別にデモソフトを使ったからってこういう動画が撮れるようになるわけじゃないです。これはあくまでもこのソフトを使って現在位置や運転情報が見られるよ~っていう様子を動画で示しただけなので…。