FlashAir、最近はこういうのがあるんですね。無線LAN機能がついていて、リモートで操作できるSDカードです。
サイズから言ってmicro SDしか挿入できない最近のタブレット/スマホでは使えませんが、SDカードが使えるデバイスならいちいちPCに挿してデータコピー、という作業から解放されます。
私はデジタルフォトフレームに入れて画像ファイルの更新に使うことにしました。
東芝(メモリ)謹製の専用ソフトもありますが、SDカードの延長として使う分には必須というわけではありません。
今回は、WebDAVでのアクセスとPythonでの操作を試します。
もくじ:
セットアップ
第4世代のFlashAir(SD-UWA W-04)を購入しました。
まずは普通のSDカードと同じようにPCに接続して設定ファイル(CONFIGファイル)を編集します。既にFAT32にフォーマットされているので間違っても再フォーマットしないように注意。
某密林で買ったのですが、届いた外装パッケージが中華。。。
まあとりあえず技適マークはついているし、ウィルスチェック(という名の気休め)もクリア。
ついでに一応イメージのバックアップを作成。普通に読むと26MB/sec (200Mbps)くらいでした。さすがClass 10。
FlashAirはCONFIG
という名前の設定ファイルを見て動作するようです。
パスは/SD_WLAN/CONFIG
です。
初期のconfigファイルはこうなっていました(値は一部伏せています)。改行はWindowsの形式(CR+LF)ですね。
1 2 3 4 5 6 7 8 9 | [Vendor] CIPATH=/DCIM/100__TSB/FA000001.JPG APPMODE=4 APPNETWORKKEY=******** VERSION=F15...W4.00.01 CID=0254...01 PRODUCT=FlashAir VENDOR=TOSHIBA |
今回設定したいのは下記の項目。
・子機モードでの自動起動
・固定IPアドレス
・WebDAV機能の有効化
・アップロード(書き込み)の許可
家庭内LANなどで、無線アクセスポイントがすでに運用されている場合は、親機モードよりも子機モードで動作させたほうが便利です。NASサーバみたいな使い方ですね。
ということで変更した後のものがこちらです。値は環境に合わせて読み替えて下さい。項目の説明は後述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | [WLANSD] ID=HomeFA DHCP_Enabled=NO IP_Address=192.168.11.xxx Subnet_Mask=255.255.255.0 Default_Gateway=192.168.11.1 Preferred_DNS_Server=192.168.11.1 [Vendor] CIPATH=/DCIM/100__TSB/FA000001.JPG APPMODE=5 APPNETWORKKEY=[password for ap] APPSSID=[ssid] VERSION=F15...W4.00.01 CID=0254...01 PRODUCT=FlashAir VENDOR=TOSHIBA UPLOAD=1 WEBDAV=2 |
設定ファイルを保存したら、PCからの接続を解除して利用予定のデバイスに挿入しなおします。
FlashAirは標準でHTTPサーバを持っているので、上記の設定がうまくいっていればブラウザでIPアドレスを指定して開くことができます。あたりまえですがドライブに挿して通電させる必要があります。
WebDAVでのアクセス
第3世代からWebDAVをサポートしているようですね。Windowsユーザなら特別なソフトをインストールすることなく標準のエクスプローラから開くことができます。
アドレスバーにパスを指定するとリダイレクトされます。
パスの形式はhttp://192.168.11.xxx/
、または\\192.168.11.xxx\DavWWWRoot
のようにします。
あとは共有フォルダと同じような感覚で操作できます。
WebDAV経由でファイルを同期したい、といった使い方には別途WebDAVクライアントのソフトを探して下さい。
設定メモ
特に説明していないところは初期値またはテキトーで大丈夫です。
初期状態の設定ファイルには[Vendor]
のセクションしかないので、必要に応じて[WLANSD]
セクションを追記します。設定を反映するにはFlashAir側の再起動(接続を解除して挿入しなおす)が必要です。
子機として起動
FlashAirはデフォルトではアクセスポイントとして起動するようなので、LANに接続させて他のマシンのアクセス先を変えずに済むよう、子機として設定します。
セクション | 項目 | 設定値 | 説明 |
---|---|---|---|
[Vendor] | APPMODE | 5 | 動作モードの指定。5は子機モード(STAモード)で自動起動する設定 |
[Vendor] | APPNETWORKKEY | 無線LANアクセスポイントへ接続するためのパスワード | |
[Vendor] | APPSSID | アクセスポイントの名前(SSID) |
APPMODE=5
とすると子機モードで起動するようになります。
アドレスの設定
今回はアドレスを固定したいので、DHCPクライアント機能を切って、アドレスを指定しました。
セクション | 項目 | 設定値 | 説明 |
---|---|---|---|
[WLANSD] | DHCP_Enabled | NO | DHCPクライアント機能の有効化、固定アドレスにしたいのでNOに設定 |
[WLANSD] | IP_Address | xxx.xxx.xxx.xxx | IPアドレス |
[WLANSD] | Subnet_Mask | 例) 255.255.255.0 | サブネットマスク |
[WLANSD] | Default_Gateway | xxx.xxx.xxx.xxx | デフォルトゲートウェイ |
[WLANSD] | Preferred_DNS_Server | xxx.xxx.xxx.xxx | DNSサーバのアドレス(優先)。代替サーバはAlternate_... に設定 |
サブネットマスク以下はよくある感じの設定。ルータの設定がわからない場合は同じLAN内のマシンで(ipやipconfig /allコマンドなどで)調べて参考にして下さい。
DHCPクライアント機能を有効化した場合(DHCP_Enabled=YES
)は、IPアドレスの設定が無視されます。
WebDAVの有効化
WebDAVを有効化すると、上記のようにエクスプローラなどWebDAVクライアントから編集できるようになります。
リードオンリーの設定もありますが、今回はアップロードもできるよう設定しています。
セクション | 項目 | 設定値 | 説明 |
---|---|---|---|
[Vendor] | UPLOAD | 1 | アップロードを許可 |
[Vendor] | WEBDAV | 2 | WebDAVの有効化。書き込みを許可 |
その他、Basic認証などのアクセス制御をかけたりといった様々な機能があります。
外で持ち歩く場合にノーガード戦法はさすがにまずいので、使い方と設定はよく検討したほうがいいと思います。
Python経由でのアクセス
せっかくHTTPサーバを積んでいるので、プログラムやスクリプトから制御できると便利ですよね。今回はPythonで操作する例を記載しておきます。
REST APIっぽいやつなのかしら、と勝手に想像していましたが、調べると、これCGIなんですね。。。
試しにざっくり書くとこんな感じ。作成した環境はWindows 10 Pro + Python3.6です。
requests
モジュールを使っているので、無い場合はpip等での追加インストールが必要です。が、そんなに複雑なことをしているわけでもなく他のモジュールで代用することも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import requests import mimetypes from datetime import datetime from traceback import print_exc class faclient: def __init__(self, path_="/"): self.path = path_ self.base_uri = "http://[ipaddr or hostname of flashair]" def get_list(self): # set params and uri param_ = {"op": 100, "DIR": self.path} uri_ = "{}/{}".format(self.base_uri, "command.cgi") # send GET request res = requests.get(uri_, params=param_) if res.status_code == requests.codes.ok: return res.text else: raise Exception(res) def retrieve_file(self, remote_path, local_dir): # build local file path local_path = os.path.join(local_dir, os.path.basename(remote_path)) # build url uri_ = "{}{}".format(self.base_uri, remote_path) res = requests.get(uri_) if res.status_code == requests.codes.ok: # write binary stream to local file with open(local_path, 'wb') as f: f.write(res.content) else: raise Exception(res) def send_file(self, local_path, rel_path=""): # handling none file case if not os.path.isfile(local_path): print("local file not found") return # set upload destination directory if not self.__set_upload_dir(rel_path): raise Exception("failed to set upload directory") # build url uri_ = "{}/{}".format(self.base_uri, "upload.cgi") # format file date dt_ = datetime.utcfromtimestamp(os.path.getctime(local_path)) buf_ = "{:07b}{:04b}{:05b}".format(dt_.year - 1980, dt_.month, dt_.day) buf_ += "{:05b}{:06b}{:05b}".format(dt_.hour, dt_.minute, int(dt_.second / 2)) ts_ = "{:08x}".format(int(buf_, 2)) # set file time res = requests.get(uri_, params={"FTIME": "0x" + ts_}) # read file data as binary with open(local_path, 'rb') as f: data_ = f.read() # config as form data: 'multipart/form-data' files_ = {'uploadFile': (os.path.basename(local_path), data_, mimetypes.guess_type(local_path)[0])} # send POST request res = requests.post(uri_, files=files_) if res.status_code == requests.codes.ok: return res.text else: raise Exception(res) def __set_upload_dir(self, dir_path=""): # set write protect param_ = {"WRITEPROTECT": "ON"} uri_ = "{}/{}".format(self.base_uri, "upload.cgi") res = requests.get(uri_, params=param_) # specify upload directory param_ = {"UPDIR": os.path.join(self.path, dir_path).replace("\\", "/")} res = requests.get(uri_, params=param_) return res.status_code == requests.codes.ok if __name__ == "__main__": try: _client = faclient() # download file _client.retrieve_file("/remote/path.ext", "path/to/localdir") # list files res = _client.get_list() print(res) # upload local file res = _client.send_file("path/to/localfile.ext", "uploads") print(res) except BaseException: print_exc() |
CGIのレスポンスはだいたい平文かHTMLで返ってきます(のでパースがちょっと面倒)。
以下に解説しておきます。
ファイルのダウンロード
ダウンロードの場合は、対象のパスを叩くとふつうにバイナリとして取得することができるので、ローカルのファイルに書き出すだけです。感覚的にcurl -O ...
と同じ。
ここではfaclient.retrieve_file()
メソッドが処理を実装しています。
1 2 3 4 5 6 7 8 9 10 11 | def retrieve_file(self, remote_path, local_dir): # build local file path local_path = os.path.join(local_dir, os.path.basename(remote_path)) # build url uri_ = "{}{}".format(self.base_uri, remote_path) res = requests.get(uri_) if res.status_code == requests.codes.ok: # write binary stream to local file with open(local_path, 'wb') as f: f.write(res.content) |
リモートのパス(SDカード内でのパス)と、ローカルディレクトリのパスを指定させ、requests.get()
メソッドで取得しています。
上記ではさぼっていますが、おそらく日本語などマルチバイト文字を含むファイル・ディレクトリ名の指定にはURLエンコードが必要になるはず。
ファイルリストの取得
情報の取得や設定関連の処理には、http://[flashair]/command.cgi
を使用します。
操作(コード)やその他パラメータをクエリストリングにシリアライズする必要があります。
サンプルでは例としてファイルの一覧を取得しています。faclient.get_list()
メソッドが処理を実装しています。
/command.cgi?op=100&DIR=...
を指定してGETリクエストを投げます。この場合、op=100
がリスト取得の指定、DIR
が取得先パスの指定になります。
1 2 3 4 5 6 | param_ = {"op": 100, "DIR": self.path} uri_ = "{}/{}".format(self.base_uri, "command.cgi") # send GET request res = requests.get(uri_, params=param_) if res.status_code == requests.codes.ok: return res.text |
レスポンスはこんな感じの文字列が返ってきます。
1 2 3 | WLANSD_FILELIST ,DCIM,0,16,19011,0 ,DSC_yyy.JPG,3096375,32,19283,6565 |
カンマ区切り(ただしファイル名にカンマを許容)でファイルやディレクトリの属性が返ってきます。
ファイルのアップロード
ファイルをアップロードするには、①SDカード(FlashAir)を挿入しているデバイスからの書き込みを禁止する手順、②アップロード先のディレクトリを指定する手順、③ファイル作成日時を指定する手順、④アップロード自体の手順の4段階が必要になります。ディレクトリの指定を省略した場合はデフォルトの、SDカード直下がアップロード先とみなされます。
アップロード関連の処理には、http://[flashair]/upload.cgi
を使用します。
ここではfaclient.send_file()
メソッドが処理を実装しています。
書き込み禁止の処理には/upload.cgi?WRITEPROTECT=ON
、アップロード先のディレクトリを指定する処理には、/upload.cgi?UPDIR=...
を使用します。
プライベートメソッドfaclient.__set_upload_dir()
を別途定義し、パラメータとしてクエリストリングに渡します。
1 2 3 4 5 6 7 | # set write protect param_ = {"WRITEPROTECT": "ON"} uri_ = "{}/{}".format(self.base_uri, "upload.cgi") res = requests.get(uri_, params=param_) # specify upload directory param_ = {"UPDIR": os.path.join(self.path, dir_path)} res = requests.get(uri_, params=param_) |
ここではGETメソッドを使います。POSTではありません。
作成日時(/upload.cgi?FTIME=0x.....
)はFAT32の仕様に合わせて8桁の16進数にフォーマットします。また、アップロード自体の処理には、ファイルをmultipart/form-data
形式でPOSTします(フォーム送信に使うやつ)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # build url uri_ = "{}/{}".format(self.base_uri, "upload.cgi") # format file date dt_ = datetime.utcfromtimestamp(os.path.getctime(local_path)) buf_ = "{:07b}{:04b}{:05b}".format(dt_.year - 1980, dt_.month, dt_.day) buf_ += "{:05b}{:06b}{:05b}".format(dt_.hour, dt_.minute, int(dt_.second / 2)) ts_ = "{:08x}".format(int(buf_, 2)) # set file time res = requests.get(uri_, params={"FTIME": "0x" + ts_}) # read file data as binary with open(local_path, 'rb') as f: data_ = f.read() # set as form data: 'multipart/form-data' files_ = {'uploadFile': (os.path.basename(local_path), data_, mimetypes.guess_type(local_path)[0])} # send POST request res = requests.post(uri_, files=files_) |
Pythonではmimetypes
モジュールを使って(完全ではありませんが)MIMEタイプを判定させることができます。
request.post
メソッドの、data
引数ではなく、files
引数に指定しなければいけない点にはちょっと注意が必要かも。
いやぁPCにいちいち挿さなくてもファイル転送ができるのは便利ですね。簡単なWebサーバとして動作するので、使い方によっては色んなアプリケーションが作成できそうです。
おわり。
コメント
[…] FlashAirで始めるワイヤレス転送 […]