Javascrip で作る FlashAir (TM) クライアントアプリ - FlashAir W-03 の挙動まとめ

東芝 FlashAir W-03 の挙動まとめ

「FlashAirをいじってわかったこと。」

https://sites.google.com/site/gpsnmeajp/electricmemo/flashair
にいろいろとまとめてくださってあり、非常に参考になります。


ここでは、ウェブプログラマ的視点で、私が気が付いたことを、ネタがかぶらない範囲でまとめておきます

ちなみに私の手持ちのFlashAir W-03のファームウェアバージョンは

FA9CAW3AW3.00.00

GET /command.cgi のキャッシュ防止には &TIME=xxxx を付ける。 → jQueryの {cache:false}は使えない。


HTTPのGETメソッドは通常のアクセスと同様に、ブラウザがキャッシュすることで同じページを2度目に開くときに時間を短縮します。が、時によってこの機能が邪魔な場合があります。
FlashAirの場合 command.cgi?op=121 でSDカードの更新状態を定期的にチェックするのですが、ブラウザがこの呼び出しの結果をキャッシュしてしまうとSDカードが更新されてもずっと同じ結果(タイムスタンプ)が返ってくるため期待した結果と異なることになります。
そこで、簡単な解決策としてアクセスするURLを少しずつ変えることでいつも新しいページを開くようにブラウザのキャッシュを回避する方法があります。
GET /command.cgi?t=10000
とアクセスした後
GET /command.cgi?t=10001
と適当に異なる番号をつければ、ブラウザは別のページだと解釈してかならず、サーバーから新しいコンテンツを取り寄せます。

ところが、FlashAirの /command.cgiは ?TIME=xxxx という書式は受け付けますが ?t=xxxx という書式だとなぜかエラー。
さらに困ったことに、、
jQuery.ajax には  { cache: false } というオプションがあって先に説明した方法でブラウザのキャッシュを回避するオプションがあるのですが、これが t=xxxx という書式でURLの末尾に追加する方法なのです。このオプションはFlashAirに対しては使えません。

SDカードにカンマ(,)を含むディレクトリ名があるとそのページの自動更新が失敗する。


ウェブブラウザで接続した際の単純なバグです。

https://flashair-developers.com/ja/documents/api/commandcgi/#100
ではカンマを含むファイル名、ディレクトリ名について言及しているだけに残念です。
List.htmに挿入するための <!--WLANSDJLST--> というキーワードは" "で囲まれた書式のため心配が要りませんが
http://flashair/command.cgi?op=100&DIR....
の結果はCSV表現なのですがカンマの含まれる名前はEXCELなどでなされる""で囲むなどの処理がされていないため、読み込みプログラム側で柔軟に解析しないと失敗します。拙作の FlashAir List.htmChromeアプリ版 FlashAir Sync はこれの対策を入れてあります。

同時にダウンロードできるファイルは1つだけ ← HTTP(WiFi)経由でファイルをダウンロード中は API の応答が停止する。


ApacheやIISなどモダンなウェブサーバーになれているとハッとさせられる挙動です。大きな画像ファイル、デジカメのRAWファイルなどある程度転送の時間がかかる場合に注意が必要です。
ファイルは1つずつしか転送できません。APIにアクセスするアプリが同時に多数ある場合は、応答のパフォーマンスに配慮する必要があります。
当初、私は、複数のディレクトリを指定してゲリラ的にローカルと同期させるようなアプリを考えていましたが、この制限によって、ファイルの自動転送は、ディレクトリごと1つ1つが正しい作法だということになりました。

※WiFi経由の転送は同時に1つに制限されますが、その間SDカードへの直接書き込みはできまるから、デジカメでの撮影が滞ることはありません。

ブラウザユーティリティー <!--WLANSDJLST--> より command.cgi?op=100 のほうが速い


SDカードに内蔵のウェブサーバーは、小電力の限界もあってパフォーマンス的に期待するのは酷なのは想像に難くありません。
結論としては、SDカード側で難しいことはさせず、複雑な処理はできる限りクライアント側でやらせるというアプローチがいいことになります。

当方では List.htm のカスタマイズを公開していますが、当初 <!--WLANSDJLST--> を使ったアプローチで作成しましたが、
ディレクトリを移動するごとに HTMLのすべての転送、js/cssなどのファイルの転送が発生し、ページの表示が完了するまで数秒の待ちが発生してしまいました。
FlashAirオリジナルの List.htm の場合も同様です。
そこで、改良版として List.htm に <!--WLANSDJLST--> を一切含まないものを作成して、ファイルのリストはAPIの
/command.cgi?op=100&DIR=....
で取得するように変更しました。
その結果ページの表示に関して2割程度のスピードアップ。さらに、ディレクトリの移動はURLをリンクするのではなく、
ページ内のJavascriptからAJAXでAPIからファイルリストを取得しなおす「シングルページアプリ」として作ることで、
ディレクトリの移動も含め劇的に快適なスピードでアクセスできるようになりました。

ファイルアップロードのHTTP通信で注意すべきこと

ファイルのアップロード upload.cgi は本家ドキュメントで書かれていないことが数点あります。

  • アップロードの multipart/form-dataの作り方で、Content-Disposition: ... filename="FILENAME.JPG"
    のように filename を必ずダブルクォーテーションでくくる必要があります。.NETの一部のAPIでこれを付けないものがあるので要注意。
  • ファイルアップロードで upload.cgi 呼び出しの戻りは text/html です。SUCCESS/ERROR ではありません。

FlashAirのウェブサーバーはブラウザのキャッシュ機構に優しくない・・・

ブラウザカスタマイズで List.htm を作成した場合、関連する jsやcssファイル、画像ファイルなどがSDカード側からロードされるのですが、どうやら、サーバー(FlashAir)のレスポンスに、サーバーがLast-Modifiedなどキャッシュ関連のヘッダーが付いておらず、唯一ETag がついているもののウェブブラウザからの If-None-Match に応答している気配がありません。したがって、ページを開くたびに関連するファイルがすべて転送されることとなり、ページが表示しきるまでの待ち時間がそれなりに生じてしまいます。

拙作 List.htm カスタマイズではディレクトリの移動を ajax でのみ更新するシングルページアプリとしたことでこの問題を回避しています。

luaスクリプトは・・・将来に期待


電子工作などの活用でいろんな記事を見ていると可能性があって面白いです。ただ、「写真を共有」「ファイルをダウンロード」的な典型的な使い方の範囲では先述の理由もあって、パフォーマンス、電力消費、安定性などからAPIを軽くたたきつつ、重い処理はウェブブラウザで・・・というアプローチが現状快適です。

たとえば、command.cgi?op=121 更新タイムスタンプの取得 はSDカードのどこかのディレクトリに変更があった場合に更新されますが必ずしも現在注目している(ブラウザで開いている)ディレクトリであるとは限りません。luaとの組み合わせで、特定のディレクトリのみをモニターする仕組みを考えたのですが、command.cgi?op=100 で変更があったかもないかもしれないファイルリストの列挙を呼び出すだけのほうが実はFlashAirの負荷が少ないんじゃないかと思ったりします。

一方で、1つのディレクトリに1万個以上の大量のファイルがある場合に、ファイルリスト取得のAPIは毎回巨大サイズのCSVファイルを転送することになり非効率です。欲を言えば、 command.cgi?op=100にページングのオプションが付けられないでしょうか。




Javascriptで作るFlashAir(TM)アプリ 4種

TOSHIBA FlashAir(TM) 職人心をくすぐる製品でしたので勢いあまって作ったユーティリティーをオープンソースコミュニティの役に立てればと放出します。

FlashAir Javascript client library

JavascriptからFlashAirのAPIをたたきます。ajaxの通信、日付、各種ステータス情報を、オブジェクト化およびカプセルしています。クライアントクラスは command.cgi?op=121 によるSDカードの更新状況の自動通知機能も装備されています。

ソースコードは

  • TypeScript(.ts)のソースコード
  • .tsをコンパイルした Javascript(.js) ファイル
  • Javascript(.ts) を uglify で圧縮した (.min.js)ファイルと .map ファイル
をパッケージしています。


以下のアプリはこのライブラリを使って作られています。

FlashAir List.htm カスタマイズ

FlashAirの隠しフォルダ SD_WLAN に List.htm というファイルを作ることでウェブブラウザ経由のページをカスタマイズできます。レスポンシブデザイン、およびサクサクパフォーマンスのカスタマイズを公開しています。ぜひお試しください。

利用はGitHubから [Download ZIP] でZIPファイルをダウンロードして SD_WLAN ディレクトリを上書きします。

FlashAirList (GitHub)





Yokin's FlashAir Sync (Chromeアプリ版)

同種の名前がいくつかあって、かぶりますが、その名の通り、クライアントとなるPCとFlashAirのファイル転送を手助けするツールです。Chromeアプリとして作られているので、Google Chromeの動作するWindows Linux Macなどで動作します。入手はGoogle WebStoreからどうぞ。ソースコードも公開(準備中)しています。


FlashAir Sync (Windows Store版)

同じJavascriptのライブラリを使用して作れるという点で、Windows 8専用のWindowsストアアプリが WinJSというJavascriptの開発プラットフォームを利用します。

https://www.microsoft.com/ja-jp/store/apps/yokins-flashair-sync/9nblggh1mtvm


FlashAir Sync (Cordova版) 希望

同じJavascriptの開発プラットフォームならAndroid iOS対応のアプリも開発ができるはずです。・・・目下遠い目標として。


Androidの非同期通知機能 Google Cloud Messaging (GCM) のサーバー側コードをC#で書いてみた

.NETの標準以外に Newtonsoft のJSON.NETを使用します。http://james.newtonking.com/pages/json-net.aspx
使い方は

    var container = new Google.GoogleCloudMessaging.Container {
        RegistrationIds = new String [] { "APA*******PQZQ" },
        Data = new { message = "Hello World!" }
    };
    var response = Google.GoogleCloudMessaging.Broadcast( "AIz************", container );

のようになります。

------------------------------

 

using System;
using System.Net;

using Newtonsoft.Json;

namespace Google
{
    public class GoogleCloudMessaging
    {
        public class Container
        {
            [JsonProperty("registration_ids")]
            public String[] RegistrationIds { get; set; }
            [JsonProperty("collapse_key", NullValueHandling=NullValueHandling.Ignore )]
            public String CollapseKey { get; set; }
            [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
            public object Data { get; set; }
            [JsonProperty("delay_while_idle", NullValueHandling = NullValueHandling.Ignore)]
            public Boolean? DelayWhileIdle { get; set; }
            [JsonProperty("time_to_live", NullValueHandling = NullValueHandling.Ignore)]
            public int? TimeToLive { get; set; }
            [JsonProperty("restricted_package_name", NullValueHandling = NullValueHandling.Ignore)]
            public String RestrictedPackageName { get; set; }
            [JsonProperty("dry_run", NullValueHandling = NullValueHandling.Ignore)]
            public Boolean? DryRun { get; set; }
        }
        public class Response
        {
            [JsonProperty("multicast_id")]
            public String MulticastId { get; set; }
            [JsonProperty("success")]
            public int Success { get; set; }
            [JsonProperty("failure")]
            public int Failure { get; set; }
            [JsonProperty("canonical_ids")]
            public int CanonicalIds { get; set; }
            [JsonProperty("results")]
            public Result[] Results { get; set; }
        }
        public class Result
        {
            [JsonProperty("message_id")]
            public String MessageId { get; set; }
            [JsonProperty("registration_id")]
            public String RegistrationId { get; set; }
            [JsonProperty("error")]
            public String Error { get; set; }
        }
        static public Response Broadcast(String API_KEY, Container container)
        {
            var request = WebRequest.Create("https://android.googleapis.com/gcm/send");
            var json = Newtonsoft.Json.JsonConvert.SerializeObject(container);
            using( WebClient web = new WebClient()){
                web.Headers.Add("Authorization", "key=" + API_KEY );
                web.Headers.Add("Content-Type", "application/json");
                byte[] result = web.UploadData("https://android.googleapis.com/gcm/send", "POST", System.Text.Encoding.UTF8.GetBytes(json) );
                String resultString = System.Text.Encoding.UTF8.GetString(result);
                var response = Newtonsoft.Json.JsonConvert.DeserializeObject<Response>( resultString );
                return response;
            }
        }
    }
}