仙石浩明の日記

2021年8月24日

M5Stack ATOM Lite を USBシリアル変換アダプタにしてみた 〜 Raspberry Pi Pico の UART コンソールを使う 〜

はじめてラズパイを買ったら意外に面白くてハマってしまった。 電子工作なんて 30年ぶりで、 半田ごてを握るには年を食いすぎている (手元がふるえる) のだけど、 ブレッドボードやら電子パーツやらをいろいろ買い揃えて、 オリンピックが終わってからの 2週間、寝る間も惜しんで楽しんでいる。

Raspberry Pi Zero WH が 1848円、 Raspberry Pi Pico が 550円、 M5Stack ATOM Lite が 1287円。 安いので次々と買ってしまった。 ラズベリーパイ (Raspberry Pi)、略してラズパイと呼ばれるようになって久しいが、 M5Stack をはじめとする ESP32 (や ESP8266) を使ったコントローラは、 何と呼ばれているのだろう?

Raspberry Pi Zero WH は普通の Linux マシンなので何でもありだが、 他はマイコン (マイクロコンピュータ) ならぬマイクロコントローラなので、 普通の OS を動かすことは難しく制約が多くて一筋縄にはいかない。

ググってみると、 開発環境として Arduino IDE を使い C (C++ ?) や Java などで開発している人が多いようだ。 30年前ならいざ知らず、 マイクロコントローラと言えども計算リソースが潤沢にある (30年前の汎用機並?) 昨今、 なぜコンパイラ言語 (しかも C や Java みたいなアセンブラと大差ない低級言語) を使うのか? インタプリタ言語なら対話的にコード片を実行して、 動作を確認しながらプログラミングできる (REPL, Read-Eval-Print Loop) ので、 開発効率が圧倒的に高い。

というわけで、 わたし的には MicroPython 一択 (ちなみに Python を使うのは今回がはじめて) なのであるが、 困ったことに Raspberry Pi Pico (以下 Pi Pico と略記) は、 開発環境である PC との通信手段が限られる。 USB コネクタが一つしかなく、 M5Stack ATOM Lite 等と違って Wi-Fi 機能もない。 つまり、 Pi Pico に USB 機器をつなぐ (Pi Pico が USB ホスト) 場合は、 USB で PC へつなぐ (PC が USB ホスト) ことができなくなるので、 PC と通信する手段が無くなってしまう。 プログラム実行中に PC と通信できなくては REPL にならない。

USB (Universal Serial Bus) がダメなら Universal じゃないシリアル通信を使えばいい、ということで Pi Pico にもシリアル通信のための UART (Universal Asynchronous Receiver/Transmitter, 調歩同期式汎用送受信機) が装備されている (ただし Pi Pico 用の MicroPython は UART では REPL できないので再ビルドの必要がある。 後述)。 PC 側でも UART 機能があれば通信できる。 というか USB や Wi-Fi が無かった時代は UART 通信 (RS-232C など) の方が一般的だった。

ところが、 いまどきの UART は 3.3V だという。 ±3~25V の信号線を使っていた RS-232 規格とは隔世の感がある。 ±25V な機器はさすがに捨ててしまったが、 いまでも手元にある USBシリアル変換アダプタは 0〜5V (TTL レベル) のものばかり。

5V を Pi Pico が扱える 3.3V まで下げるのは抵抗を使って分圧すればいいが、 その逆、 つまり Pi Pico から PC へ 5V の信号を伝えるのは少々やっかいである。 3.3V のままでも PC に H レベルと認識してもらえなくもないが、 マージンが狭くなるのは否めない。 もちろん 115200bps とかなら問題も起きないだろうが、 現代なら 1.5Mbps くらいは出したいところ。

もちろん素直に 3.3V 対応の USB to TTLシリアルアダプタを買えばいいのだが、 USBシリアル変換アダプタを既に (何個も) 持っているのに新たに買うのはモッタイナイ気がするし、 元々 200〜300円くらいしかしないパーツを、 本体と同じくらいの送料を払って買うのも業腹である (こんど秋葉原へ行ったときにでも買おうっと)。

Double Pico ! ATOM Lite as a Serial Converter to Pi Pico

要は 3.3V な UART があればいいわけで、 M5Stack ATOM Lite (以下 ATOM Lite と略記) を USB シリアルアダプタにしてしまえばいい!と思いついた。 つまり ATOM Lite も Pi Pico と同様 USB で REPL が使えるが、 ATOM Lite の REPL ではなく、 ATOM Lite (写真上) と UART シリアル (写真上の 3本のジャンパー線, うち黒は GND) でつないだ先の Pi Pico (写真下) の REPL を使おうという目論見。 ATOM Lite は PC と Pi Pico との通信を中継するだけ。 ATOM Lite もチップの名前は ESP32-PICO-D4 なのでダブルピコ!

PC (開発環境) ←──USB──→ ATOM Lite ←──UART──→ Pi Pico

MicroPython では flash メモリに boot.py を置いておくと起動時に実行してくれる。 ATOM Lite を常にシリアルアダプタとして使いたいわけではないので、 ATOM Lite のボタンを押しながら起動したときだけシリアルアダプタとして機能するようにしてみた。 シリアルアダプタとして動作中はボタン中央の LED が緑色に点灯する。 もう一度ボタンを押すと LED が消灯し、 通常の REPL モードになる。 boot.py に以下のプログラムを追記した:

import machine
import sys
import neopixel
import utime
import _thread

btn = machine.Pin(39, machine.Pin.IN)
if btn.value():
    sys.exit()

pxls = neopixel.NeoPixel(machine.Pin(27), 1)
pxls[0] = (0, 25, 0)
pxls.write()
start_time = utime.time()

uart = machine.UART(1, 115200, tx=21, rx=25)
done = False

def thread():
    global done
    while not done:
        c = sys.stdin.read(1)
        if c == "\n":
            uart.write("\r\n")
        else:
            uart.write(c)
    _thread.exit()

_thread.start_new_thread(thread,())

while not done:
    if btn.value() == 0:
        if utime.time() - start_time > 10:
            done = True
    if uart.any() > 0:
        sys.stdout.write(uart.read(1))
    else:
        utime.sleep_ms(1)

pxls[0] = (0, 0, 0)
pxls.write()

ATOM Lite の GPIO 21番ピンを UART TX として、 GPIO 25番ピンを UART RX として使い、 それぞれ Pi Pico の UART0 RX および UART0 TX につなぐ。 Pi Pico の VBUS と GND に 5V 電源を供給する (写真右下の赤と黒のジャンパー線) ことで USB コネクタを使わずに空けておける。

More...
2021年7月18日

PayPay STEP の新基準をクリアしてみた 〜住民税と健康保険料の支払で1.5%還元〜

今月 7月から PayPay STEP の条件が改訂された。 先月までは合計 10万円以上 PayPay 残高払いすれば、 翌月の還元率が 0.5% から 1% へ 0.5 ポイントアップした。 ところがこれからは合計 5万円以上かつ 30回以上 PayPay 残高払いしないと 1% にならない。 10万円が 5万円に下がったが、 一ヶ月に 30回以上という条件は厳しすぎる。

なぜ還元率を 1% にしたいかというと、 住民税と健康保険料、合わせて 150万円の納付を PayPay 請求書払いで行いたいから。 還元率が 0.5% から 1% へ上がると 7500円ほど還元額が増える。 1% を超える高還元率のクレジットカードは多いが、 たいてい納税等には使えなかったり、 使えても上限額が低かったりする。 一ヶ月に 150万円までの納税等が 1% 還元でできる PayPay は貴重。

6月に健康保険料の第1期分 99,000円を PayPay 請求書払いで納付した。 で、その後ランチで 1133円を PayPay 残高払いした。 これで合計 10万円以上になったので、めでたく翌月 7月の還元率が 1% になった。

Toyonaka PayPay 20% campaign

7月になって請求書払いの還元率が 1% になったのが確認できたので、 住民税と健康保険料の残り (116万円ほど) を全て払ってしまおうと思ったが、 豊中市 x PayPay 20% 還元キャンペーンが始まってしまった。

私は普段 PayPay 残高払いを使わないが、 それは還元率が 0.5% と低いから。 PayPay しか使えないお店では PayPay クレジットカード払いを使うが、 PayPay 残高払いと違って PayPay クレジットカード払いだと PayPay の還元は無いし、 PayPay STEP の回数としてカウントされない。

たいていのお店でもっと高い還元率の支払手段があるし、 PayPay クレジットカード払いなら PayPay に登録したクレジットカードの還元 (私の場合は 2%) が得られる。 わざわざ低還元率の PayPay 残高払いを使う理由は何もない。 が、20% も還元してくれるとなると話は別である。

豊中市に住んでいて、 かつ普段ランチを食べる店や、 最寄りのドラッグストアが 20% 還元の対象店舗なので、 一ヶ月に 30回くらい PayPay 残高払いを使うのは造作もない。 これで 8月も還元率 1% を達成できるメドが付いた。

こうなってくると欲が出て、 さらに上の 1.5% 還元を目指したくなった。 還元率が 1% から 1.5% へ 0.5 ポイント上がると、 住民税と健康保険料の支払で付与される PayPay ボーナスが 7500円増える。 ただし付与上限が 15,000円なので、 1.5% 還元の場合は 1ヶ月に 100万円までの支払しか対象にならない。

1.5% 還元を得るには、以下の 4条件を満たさなければならない:

1) PayPay 支払
合計 5万円以上かつ 30回以上 PayPay 残高払い

2) 次の対象サービスのうち 3つ以上を利用
PayPayモール または Yahoo!ショッピング
PayPayフリマ または ヤフオク!
Yahoo!トラベル
ebookjapan
LOHACO by ASKUL
ただし、 ebookjapan は 300円以上の購入、それ以外は 1000円以上の購入が必要。

3) Yahoo!プレミアム会員登録
あるいはソフトバンクスマホユーザーかワイモバイルユーザー。

4) PayPayアカウントとYahoo! JAPAN IDを連携

条件 1) はメドが付いた。条件 3), 4) は登録するだけの話なので簡単。 私の場合 Yahoo!プレミアム会員登録が 6ヶ月無料だった。 問題は条件 2) である。 いずれのサービスも私は利用したことがない。 もちろん高々 1000円利用するだけだから、 要らないものを買ってもよければすぐ達成できる。 しかし還元率を 0.5 ポイント上げるために 3000円 (ebookjapan を利用する場合は 2300円) をドブに捨ててしまっては本末転倒である。 出来る限り有意義な買物をしたい。

More...
Filed under: その他 — hiroaki_sengoku @ 08:30
2020年9月16日

IFTTT のアプレットが 3個に制限されてしまったので、IFTTT を使わずに スマート家電リモコン RS-WFIREX4 をコントロールしてみた 〜 RS-WFIREX4 の通信プロトコルの解析 hatena_b

IoT機器を IFTTT (IF This Then That) に登録すると、 自前のプログラムからコントロールできるようになる。 つまり IFTTT の webhooks を使うことで、 IFTTT の特定の URL を自前のプログラムからアクセスするだけで IoT機器がコントロールできる。 例えばこんな感じ:

senri:~ $ curl https://maker.ifttt.com/trigger/light_on/with/key/dD-v7GCx46LnWaF1AD9nwSUeA_N1ALvDHKS57cP1_Md
Congratulations! You've fired the light_on event

「light_on」の部分は任意に定めることができる。 この例では照明を点灯させている。

Nature Remo のように API を公開している IoT機器なら、 自前のプログラムから API を直接たたけばよいが、 残念ながら IoT機器の多くが API 非公開なので、 IFTTT が唯一のコントロール手段となっていた。 IoT機器の操作一つ一つ (例えば light_on) に、 IFTTT アプレットを作ることになるので、 私の場合は 50個以上のアプレットを作っていた。

Get IFTTT Pro

ところが!

有料版の IFTTT Pro が新たに発表され、 従来の無料版は登録できるアプレットが 3個に制限されてしまった。 有料版なら無制限にアプレットを作ることができるが、 無料版だと最大 3個しかアプレットを作ることができない。

かくなる上は、 IoT機器の API (通信プロトコル) を解析して IFTTT 抜きで IoT機器をコントロールするしかない。 いままでも IFTTT に登録できない IoT機器については API を解析していたので、 なんとかなるだろう。

IoT機器のほとんど (全て?) がスマホからコントロールできるので、 root 権限を取得できる Android 端末があれば、 スマホのアプリと IoT機器との間の通信を tcpdump 等で観察することができる。 通信が暗号化されていなければ API を解析するのは (比較的) 容易。

RS-WFIREX4

というわけで、 ラトックシステム社 スマート家電リモコン RS-WFIREX4 の API を解析してみた。 幸い、 RS-WFIREX4 の Android 用アプリ スマート家電コントローラ は、 家中モード (スマホと RS-WFIREX4 が同一セグメントにある場合) では通信が暗号化されていない。 スマホ上で tcpdump を実行することで通信 (TCP/IP) 内容を見ることができた。

RS-WFIREX4 は温度、湿度、明るさを計測することができる。 アプリが RS-WFIREX4 の TCP ポート 60001番に対して 5バイトのデータ 「AA 00 01 18 50」 を送信すると、 RS-WFIREX4 から 13バイトのデータ 「AA 00 09 18 00 01 5E 00 E5 00 0A B2 08」 が返ってきた。 最初の 5バイト 「AA 00 09 18 00」 は部屋の温度等に関係なく常に同一だったので、 ヘッダ (つまり計測データは含まれない) と考えられる。

この手の通信プロトコルでは、 可変長のデータを扱うためにヘッダにペイロード (ヘッダ以外のデータ本体) の長さが含まれる (ことが多い)。 この例ではヘッダ以外のデータの長さは 13 - 5 = 8 バイトなので、 おそらく 「00 09」 が (ビッグエンディアンの) ペイロード長だろうとあたりをつける。 つまりヘッダは 「AA 00 09 18」 の 4バイトで、 続く 9バイト 「00 01 5E 00 E5 00 0A B2 08」 がペイロードということになる。

┌─┬─┬─┬─┬─┐
│頭│ペイ長│測│検│
└─┴─┴─┴─┴─┘
│←─ヘッダ─→│ペ│

アプリが送信したデータ 「AA 00 01 18 50」 も、 先頭が 「AA」 (図中では 「頭」 と略記) であることから同じフォーマットである可能性が高い。 つまり 「00 01」 がペイロード長 (図中では 「ペイ長」 と略記) で、 「50」の 1バイトがペイロードなのだろう。 「50」は 「計測データを送信せよ」 という命令の可能性も無くはないが、 おそらく後述するチェックサムだろう (図中では 「検」 と略記)。

そして、 ヘッダ末尾の 「18」 のほうが、 「計測データを送信せよ」 という命令である可能性が高い (図中では 「測」 と略記)。 RS-WFIREX4 の応答のヘッダの末尾も 「18」 だが、 これは命令 「18」 に対する応答であることを示しているのだろう。

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│頭│ペイ長│測│0│湿度%│温度℃│明るさ│安│検│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│←─ヘッダ─→│←─────ペイロード─────→│

部屋を明るくしたり暗くしたり、 温度を上げたり下げたりしたときに、 RS-WFIREX4 からの応答がどのように変化するか調べることで、 ペイロードの 2, 3バイト目 「01 5E」 (10進数で 350) は、 「湿度 35.0 %」 を示していると推測できた。 以下同様に、 4, 5バイト目 「00 E5」 (10進数で 229) は、 「温度 22.9 ℃」 を、 6, 7バイト目 「00 0A」 (10進数で 10) は、 「明るさ」 を示している。

ペイロードの 8バイト目 「B2」 は、 RS-WFIREX4 の電源を入れた瞬間は 0 で、 時間の経過と共に増大し、 充分時間が経つと 255 になる。 RS-WFIREX4 は電源投入後 30分間はセンサーが使えないので、 おそらくセンサーの安定度合い (0〜255) を示していて、 この数値が一定以上 (255?) でないとセンサーの値が正確でないことを示しているのだろう。

末尾の 1バイトは後述するチェックサムと思われる。 なぜなら、 末尾の 1バイトを除くペイロードの内容が同じ (つまり湿度・温度・明るさ・安定度の組合わせが同一) 応答データであれば、 末尾の 1バイトも (少なくとも私が観察した範囲では) 同じ値になっているから。

AA 00 AE 11
00 00 AA
22 11 04 04 05 04 04 0D 04 0D 04 04 05 0C 05 04
04 04 05 04 04 0D 04 04 05 04 04 0D 04 04 05 0C
05 04 04 0D 04 04 05 04 04 0D 04 04 05 04 04 04
05 04 04 04 05 04 04 0D 04 0D 04 04 05 0C 05 04
04 04 05 0C 05 04 04 0D 04 04 05 04 04 0D 04 04
05 04 04 FF FF FF 07 23 10 05 04 04 04 05 0C 05
0C 05 04 04 0D 04 04 05 04 04 04 05 0C 05 04 04
04 05 0C 05 04 04 0D 04 04 05 0C 05 04 04 04 05
0C 05 04 04 04 05 04 04 04 05 04 04 04 05 0C 05
0C 05 04 04 0D 04 04 05 04 04 0D 04 04 05 0C 05
04 04 04 05 0C 05 04 04 04 05
69

次に赤外線を発射させて家電をコントロールしてみる。

← 左の 178バイトのデータをアプリが送信すると、 RS-WFIREX4 が発射した赤外線を受けて天井照明が点灯し、 RS-WFIREX4 から 6バイトの応答 「AA 00 02 11 00 D1」 が返ってきた。

送信データの最初の 4バイト 「AA 00 AE 11」がヘッダで、 ペイロードの長さが 00AE (10進数だと 174) であることが分かる。 ヘッダ末尾の 「11」 が、 「赤外線を発射せよ」 という命令なのだろう (図中では 「射」 と略記)。 アプリを操作して RS-WFIREX4 にいろいろ (長さが異なる) 赤外線を発射させてみたところ、 3行目 「22 11 04 04 ...」から始まる 170バイトが赤外線の波形データ (後述) で、 その直前 (2行目) の 「00 AA」 (10進数で 170) が赤外線の波形データの長さ (図中では「デー長」と略記) を表わしているようだ。

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│頭│ペイ長│射│0│デー長│赤外線の波形データ│検│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│←─ヘッダ─→│←─────ペイロード─────→│

ペイロード末尾の 1バイト 「69」 の算出方法は不明だが、 赤外線の波形データによって値が変わることと、 末尾であることからチェックサムのようなものと思われる。 もちろん単純なチェックサムではなく、 なんらかのエラー検出符号のようなものなのだろう (図中では 「検」 と略記)。 この 1バイトは、 「ペイロード長」には含まれるが、 「赤外線の波形データの長さ」には含まれない (12月18日追記: 「射」をヘッダではなくペイロードに含めて、その代わり「検」をペイロードから外すべきだった。後述)。

このペイロード末尾の 1バイトだけ異なるデータを送信すると RS-WFIREX4 はこの 178バイトの送信データ全体を無視する。 赤外線も発射しないし、 何の応答も返さない。 前述したように家中モードでは何のセキュリティもないので、 この末尾の 1バイトが (正規の) アプリから送信されたことを保証する唯一の認証手段なのだろう。 ここでは、 RS-WFIREX4 に対する送信データ (あるいは RS-WFIREX4 からの応答データ) におけるこの末尾の 1バイトを「チェックサム」と呼ぶことにする。

170バイトの赤外線の波形データは、 1バイト目の 「22」(10進数だと 34) が赤外線の ON の区間を表し、 2バイト目の 「11」が赤外線の OFF の区間を表し、 以下同様に奇数バイト目が赤外線の ON の区間を表し、 偶数バイト目が赤外線の OFF の区間を表す。 各バイトの数値の 1/10000 が各区間の秒数になる。 例えば 「04」 の場合は 0.4ミリ秒になり、 家製協(AEHA)フォーマットの 1T 区間に相当する。 1バイト目の 「22」は 8T 区間、 2バイト目の 「11」は 4T 区間、 8バイト目の 「0D」は 3T 区間に相当する。

IR wave

つまり赤外線の波形データの最初の行 (3行目) は、 赤外線 ON が 8T (3.2ミリ秒) 続き、 次に OFF が 4T (1.6ミリ秒) 続き、 以下 ON 1T, OFF 1T, ON 1T, OFF 1T, ON 1T, OFF 3T, ON 1T, OFF 1T, ON 1T, OFF 3T, ON 1T, OFF 1T という波形になる (上図 ↑)。 ただし ON の区間は赤外線が点きっぱなしになっているのではなく、 38kHz の赤外線パルス (デューティ比 1/3) を送信している。

この赤外線の波形データは、 アプリで 「リモコンデータ受け渡し⇒エクスポート⇒メールで送信」 を行うことで得られる XML データに含まれる (<code>...</code> の部分)。 あるいは他の学習リモコンの赤外線波形データを変換しても良い。

実を言うと、 ここまでは昨年の段階で解析済だった。 ペイロード末尾の 1バイトの算出方法が判明したら公開しようと思っていたのだが、 いろいろ他にも忙しくて :-) 放置してしまっていた (12月18日追記: 単なる CRC-8 だと判明。後述)。 アプリを逆コンパイルするのは骨が折れるのと、 算出しなくても tcpdump で見れば値が得られるので実用上は困らなかったから。 で、今回 IFTTT が有料化したので急遽公開することにした次第。

チェックサムの算出方法が分からないといっても高々 1バイトである。 256通りなんてブルートフォース攻撃というほど brute でもない。 幸い、 RS-WFIREX4 はチェックサムが違うデータを立て続けに受信しても、 異常動作することはないようだ (もちろん常時チェックサム違いのデータを送信することは推奨できない)。

赤外線の波形データごとに 00 〜 FF まで 256通りのチェックサムを試して、 RS-WFIREX4 から応答が返ってきたら、 赤外線の波形データにそのチェックサムを付加して記憶しておけばよい。 赤外線の波形データの前の 3バイトおよびヘッダは、 発射する際に都度算出すれば良い。

チェックサムが正しい場合、 RS-WFIREX4 は赤外線を発射して 「AA 00 02 11 00 D1」 を返す。 ヘッダ末尾の 「11」 は、 命令 「11」 (赤外線を発射せよ) の応答であることを示す。

┌─┬─┬─┬─┬─┬─┐
│頭│ペイ長│射│0│検│
└─┴─┴─┴─┴─┴─┘
│←─ヘッダ─→│←ペ→│

ペイロードは 「00 D1」 の 2バイトだが、 ペイロードが 2バイト以上の場合、 ペイロードの先頭は常に 「00」 であるようだ (図中では 「0」 と表記)。 末尾の 「D1」 はチェックサムだろう。 実質的にチェックサムだけのペイロードならば、 1バイトのペイロードで充分だと思うが、 アプリが送信するデータの場合は 1バイトのペイロードが有り得ても、 RS-WFIREX4 が返す応答データの場合は常に 2バイト以上になるのかもしれない。

RS-WFIREX4 をコントロールする perl スクリプト wfirex.pl を以下に示す:

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use IO::Socket::INET;

my %Ir;
$Ir{'on'} = pack("H*", "221104040504040D040D0404050C050404040504040D04040504040D0404050C0504040D04040504040D040405040404050404040504040D040D0404050C05040404050C0504040D04040504040D0404050404FFFFFF07231005040404050C050C0504040D040405040404050C05040404050C0504040D0404050C05040404050C050404040504040405040404050C050C0504040D04040504040D0404050C05040404050C050404040569");
$Ir{'off'} = pack("H*", "221005040404050C050C0504040D040405040404050C05040404050C0504040D0404050C05040404050C0504040405040404050C050C050C050C0504040D040405040404050C050C05040404050C0504040405FFFFFF07221104040504040D040D0404050C050404040504040D04040504040D0404050C0504040D04040504040D0404050404040504040D040D040D040D0404050C050404040504040D040D04040504040D040405040437");
$Ir{'small'} = pack("H*", "221104040405040D040D0404040D040504040504040D04040405040D0404050C0504040D04040504040D04040405040404050404050C050C050C0504040D04040405040D040D040D04040504040D0404040504FFFFFF08221104040405040D040D0404050C040504040405040D04040405040D0404050C0405040D04040405040D04040504040404050404040D050C050C0504040D04040504040D040D040D04040405040D0404040504E9");
$Ir{'aoff'} = pack("H*", "211005040404040d0404040d040404040504040c050c040405040404040d040c0405040404040504040405040404040405040404050404040404050c0404050404040405040404040504040c0504040404050404040d040404040504040404050404040c0504040c050c040d040c040d040c04d7");
$Ir{'study_on'} = pack("H*", "2210050C04050404040405040404050404040405040404050404040D0404040504040405040404050404040D040D040404050404040504040405040C0504040D0404040D040D040C050C0405040C0504040D04040405040404050404040405040404050C050C040D040C050C050C040D040D0404050C040405040404050404040504040C0504040D040C050C050C040D040D0404040D0404050404040504040C050C050C0405040C050C040D040D0404040504FF2E2111040C05040405040404050404040404050404040504040405040C0504040404050404040504040405040D040C040504040405040404050404040D0404050C0405040C050C040D040D0404040D0404050C04050404040504040404050404040504040D040C050C040D040D040C050C040D0404050C040504040405040404050404040D0404040D040D040D040C050C050C0405040C0504040404050404040D040D040C0504040D040C050C050C0405040404FF292111040D04040405040404050404040504040405040404050404040D0404040504040405040404050404040D040D040404050404040504040404050C0405040C0504040D040C050C040D0404050C0405040C05040404050404040504040404050404040D040D040C050C050C040D040C050C0504040D040404040504040405040404050C0504040C050C050C040D040D040C0504040D0404040504040404050C040D040D0404050C040D040C050C05040404042F");
$Ir{'study_off'} = pack("H*", "2210040D04050404040504040405040404040504040404050404040D0405040404050404040503050404040D040D040404050404040504040405040C0504040D0404040D040D040C040D0405040C0504040D04040405040404050404040504040405040C050C040D040D040C050C040D040D040C050C04050404040504040405040404050404040D040D040C050C040D040D0404040D0405040404040405040D040D040C0405040C050C040D040D0404040504FF2E2111040D04040405040404050404040504040405040404050404040D0404040504040405040404050404040D040C050404050305040404050404040D0405040C0504040D040C050C040D0404050C0405040C05040404050404040405040404050404040D040D040C050C040D040D040C050C040D040D04040504040405040404050404040504040C050C050C040D040C050C0504040D0404040504040404050C040D040D0404040D040D040C050C0504040404FF292210050C04050404040504040405040404050404040404050404040D0405040404050404040404050404040D040D040404050404040504040405040C0504040D0404040D040D040C050C0405040C0504040D04040405040404050404040405040404050C040D040D040C050C040D040D040D040C050C04050404040405040404050404040504040D040C050C040D040D040D0404040D0404040504040405040D040C040D0405040C040D040D040D040404050408");
$Ir{'study_aoff'} = pack("H*", "2c2b070f0610060f061006050605060f060506050605060406050610060f0605061006050604060506050605060505100610060f06100610060f0610060f060506050605060506040605060506050605060f06100605060f0610060506040605060506050605050506050605061005100610060506040605060506050605050506050610060505100610060505100610060505332d2b0610060f0610061005050605061006050505060506050605060f06100605060f0605060506050605050506050610060f0610061005100610060f06100605060506040605060506050605050506050610060f06050610060f06050605060506050505060506050605060505100610060f06050605060506050505060506050605060f06050610060f06050610060f06050675");

our ($opt_v, $opt_s);
getopts('vs') || help();
my $ip = shift || help();
my $command = shift || help();

if ($command eq "get") {
    my ($il, $te, $hu) = get_wfirex($ip);
    if (defined $il) {
        $te /= 10;
        $hu /= 10;
        print "il=$il te=$te hu=$hu\n";
    } else {
        print "wfirex get TIMEOUT $ip\n";
    }
} elsif (defined $Ir{$command}) {
    my $ret;
    if ($opt_s) {
        my $ir = substr($Ir{$command}, 0, length($Ir{$command})-1);
        for (my $i=0; $i < 256; $i++) {
            my $checksum = pack("C", $i);
            printf("try %02x ...\n", $i);
            $ret = send_wfirex($ip, $ir . $checksum);
            if ($ret) {
                printf("success ! checksum=%02x ret=%02x\n", $i, $ret);
                last;
            }
            sleep 1;
        }
    } else {
        $ret = send_wfirex($ip, $Ir{$command});
    }
}
exit 0;

sub get_wfirex {
    my ($ip) = @_;
    my $sock = IO::Socket::INET->new(
        PeerAddr  => $ip,
        PeerPort  => 60001,
        Proto     => "tcp",
        Timeout   => 5,
        );
    if ($sock) {
        print $sock "\xaa\x00\x01\x18\x50";
        my $buf;
        my $flags;
        $sock->recv($buf, 256, $flags);
        my @data = unpack("CCCCCnnnCC", $buf);
        close($sock);
        print join(" ", @data) . "\n" if $opt_v;
        return ($data[7], $data[6], $data[5]);
    }
    return undef;
}

sub send_wfirex {
    my ($ip, $ir) = @_;
    my $len = length($ir) - 1;
    $ir = "\xaa" . pack("n", $len+4) . "\x11\x00" . pack("n", $len) . $ir;
    my $sock = IO::Socket::INET->new(
        PeerAddr  => $ip,
        PeerPort  => 60001,
        Proto     => "tcp",
        Timeout   => 5,
        );
    if ($sock) {        
        print $sock $ir;
        my $buf;
        my $flags;
        $sock->recv($buf, 256, $flags);
        my @data = unpack("CCCCCC", $buf);
        close($sock);
        if (@data) {
            print join(" ", @data) . "\n" if $opt_v;
        }
        return $data[5];
    }
    return undef;
}

sub help {
    print <<EOF;
Usage: wfirex <opt> <IP> <com>
opt:   -v   ; verbose
       -s   ; scan checksum
com: get    ; get sensor value
EOF
    print "     " . join(" ", sort keys %Ir) . "\n";
    exit 1;
}

連想配列 %Ir に赤外線の波形データを 16進数の文字列で格納しておく。 末尾の 2文字 (つまり 16進数 1バイト) がチェックサム。 チェックサムが不明のときは、 とりあえず「00」をつけておいて「-s」オプションでチェックサムを探索する。

senri:~ $ ./wfirex -vs wfirex4l on
try 00 ...
try 01 ...
try 02 ...
try 03 ...

  … 中略 …

try 67 ...
try 68 ...
try 69 ...
170 0 2 17 0 209
success ! checksum=69 ret=d1

赤外線の波形データ $Ir{'on'} のチェックサムが 「69」 (16進数) であることが判明したので、 とりあえずつけた末尾の 「00」 を 「69」 で置き換える (前掲のスクリプトは置き換え済)。

More...
2020年9月2日

サーバの MAC アドレスを偽装 (MAC spoofing) してエッジ・スイッチの MAC アドレスと同一にしてみた 〜 プロバイダの接続台数制限を回避する

多くの機器で MACアドレス (Media Access Control address) は偽装できる。 例えば Linux サーバなら次のような感じ:

# ip link set dev eth0 down
# ip link set dev eth0 address 00:11:22:33:44:55
# ip link set dev eth0 up
# ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff

インタフェースが立ち上がった状態では変更できないので、 まず eth0 を down させているが、 サーバの起動時などインタフェースが立ち上がる前なら、もちろん不要。 そして MACアドレスを 「00:11:22:33:44:55」 に偽装している (MAC spoofing)。

     インターネット
        │
        │
   ┌────┴────┐
   │ マンション共用部│
   │   のルータ  │
   └─┬┬┬┬┬┬┬─┘
 ┌───┘│││││└───┐
 │ ┌──┘│││└──┐ │
 │ │ ┌─┘│└─┐ │ │
 : : :  │  : : :
  各戸へ   │   各戸へ
        │
     ┌──┴──┐
     |スイッチE|
     └┬─┬─┬┘
  ┌───┘ │ └───┐
  │     │     │
  :  ┌──┴──┐  :
 各部屋 |スイッチR| 各部屋
     └┬┬┬┬┬┘
  ┌───┘│││└───┐
  │ ┌──┘│└──┐ │
  ↓ ↓   ↓   ↓ ↓
  他のPC  サーバ  他のPC

私の自宅内の LAN において、 サーバ (左図下端) の MACアドレスを偽装して、 スイッチE (マンション共用部のルータにつながるエッジ・スイッチ) の MACアドレスと同一にしてみた。

なぜこんなことをするか?を、順を追って説明する:

マンション共用部のルータ (以下、「ルータM」と略記) は、 各戸に最大 5個のグローバル IPアドレスを割当てる。 つまり、 「ルータM」は各戸 (マンション専有部) ごとに接続された機器を数えていて、 接続を検知した順に先着 5台にのみグローバル IPアドレスを割当てる。

割当てられるグローバル IPアドレスが 5個なのではなく、 先着 5台の機器のみに、 DHCP リクエストがあれば IPアドレスを割当てる点に注意。 DHCP リクエストを行わない機器の MACアドレスがルータM に届いてしまえば、 それも 1台としてカウントされる。

困ったことにスイッチE (GS108E) は、 (L2スイッチのくせに) MACアドレスを持っていて、 定期的に UDP ブロードキャスト (NSDP) を行う。 このブロードキャストがルータM に届くと、 スイッチE が「接続機器」とみなされてしまい、 貴重なグローバル IPアドレス枠が一つ浪費されてしまう

NSDP のブロードキャストを止めることが可能ならそれが一番だが、 残念ながら GS108E の管理ツール (Windows アプリ) にはそういう設定項目は無い。 スイッチE の MACアドレスがルータM に届くのを阻止する術は無さそうだ。 もちろん、 スイッチE とルータM との間にブリッジを挟んでブロードキャストをフィルタリングすれば阻止できるが、 そんなボトルネックは作りたくない。

スイッチR (GS116E ポート数が異なる他は GS108E と同等) も同様に MACアドレスを持っていて NSDP のブロードキャストを行うし、 もちろん (グローバル IPアドレスを持たない) その他の PC 等も様々なパケットを送信するが、 いずれもルータM と直接つながっていないので、 VLAN を設定することでパケットがルータM へ届くのを阻止することが可能。

上記ネットワーク図には、 家庭内LAN に必ずある NATルータ (Wi-Fiルータ等) が見当たらないが、 Linux サーバが NATルータの役割を果たしている。 GS108E のようなタグVLAN 機能付スイッチを使うと、 物理的な配線にとらわれずに自在に LAN を構成できるので便利。

つまり、 MACアドレスがルータM に届くのを阻止できないのは、 ルータM と直接つながっているスイッチE のみ、 ということになる。

そこで、 スイッチE が「接続機器」と見なされるのが避けられないなら、 逆にスイッチE の MACアドレスでグローバル IPアドレスを取得してやろうと考えた次第。 サーバの MACアドレスを偽装してスイッチE の MACアドレスと同一にすることで、 このサーバは無事グローバル IPアドレスを取得できた。

もちろん、 これでは同一セグメント内に同じ MACアドレスを持つ機器が存在することになってしまう。 これはネットワークの教科書的には、 決してやってはいけないことだ。

一番の問題はスイッチの MACアドレス学習が混乱する点。 上図でいうと「スイッチR」 (部屋ごとに設置しているスイッチ) は、 上の「スイッチE」からも下の「サーバ」からも、 同じ MACアドレスを送信元とするパケットが届いてしまう。

また、2番目の問題として、 スイッチE に自身の MACアドレス宛のパケットが届いたとき、 それを正しくサーバへ中継するのではなく、 自身宛と見なして中継しない (そのまま捨ててしまう) 恐れがある。

まず 1番目の問題は、 スイッチE のブロードキャストの間隔が充分長ければ、 実用上の問題は起きないだろうと考えた。 確かに、 スイッチE のブロードキャストがスイッチR に届けば、 スイッチR で誤学習が起きる。 この状態でスイッチR にサーバ宛のパケットが届くと、 スイッチR はサーバへ送らずに、 スイッチE へ送ってしまう。

が、サーバは常時通信を行っているわけだから、 スイッチR には速やかにサーバからパケットが届いて誤学習状態は直ちに解消される。 一時的な誤学習は、 それが低頻度かつ短時間であればパフォーマンス上の問題は起きないだろう。

2番目の問題は、 スイッチの機種にも依存するが、 少なくとも私が使ってる GS108E-100JPS の場合は問題にならない。 つまり自身の MACアドレス宛のパケットが届いても、 それを自分宛とは見なさず、 他のパケットと同様に中継する。

例えばインターネットからサーバ宛に届くパケットの場合、 ルータM はサーバの MACアドレス (つまりスイッチE の MACアドレス) を宛先としたパケットを送信するが、 このパケットはスイッチE において何事もなく中継されて、 サーバに正しく届く。

なお、 GS108E-100JPS の後継機 GS108E-200JPS は、 管理ツール以外に WWW ブラウザで管理することもできる。 つまり (簡易な) Web サーバを内蔵しているわけで、 このようなスイッチの場合は、 スイッチの MACアドレスを宛先とするパケットは、 この Web サーバによって受信されてしまい、 このスイッチにおいては中継されないと予想される。

実際に GS105E-200JPS (ポート数が異なる他は GS108E-200JPS と同等と考えられる) で実験してみたところ、 スイッチの MACアドレスを宛先とするパケットは、 スイッチで中継されなかった。 したがって GS105E-200JPS や GS108E-200JPS を、 スイッチE として使うことは、 グローバル IPアドレス枠を一つ浪費することになるので適切ではない。

もちろん、 スイッチE として一番適切なのは、 無用なブロードキャストを吐かない (あるいは吐かない設定が可能な) スイッチである。 接続台数に制限があるプロバイダを利用する際は、 エッジ・スイッチの挙動の細かな違いにも気を配りたい。

Filed under: システム構築・運用 — hiroaki_sengoku @ 09:35
2019年12月10日

学習リモコンの赤外線波形データを変換してみた 〜 Nature Remo で取得した波形データを PC-OP-RS1 用に変換

人感センサ (人の動きを感知するセンサ) 付であることに魅力を感じて IoT な学習リモコン Nature Remo を買ったら、 人の動きをトリガーにした IFTTT との連携ができないばかりか、 センサの感度もあまりよくなかった。 仕方ないので人感センサを新たに買ってみた

人感センサとしての感度は、 だんぜんこの +Style ORIGINAL スマートセンサー(人感) PS-SMT-W01 のほうがいい。 IFTTT と連携できないので他の IoT 機器との連携を考えている場合は注意が必要だが、 私は IFTTT をショートカットするので無問題。 思わず買い増ししてしまった。

Nature Remo から人感センサを引き算したら、 残りは「学習リモコン」ということになるが、 そこで思い出したのが 13年前に買ったパソコン用学習リモコン PC-OP-RS1。 いま流行りの IoT では無いが、 サーバが置いてある部屋で使うのであれば IoT である必要はなく、 むしろ PC-OP-RS1 のように (ネットを介さず) USB で直接コントロールできるほうが、 赤外線を発射するまでの遅延が少なくてすむ。

学習リモコン PC-OP-RS1 と人感センサ PS-SMT-W01 を組合わせれば Nature Remo は不要? と思ったので押し入れの中から PC-OP-RS1 を発掘した。 ところが、 家電のリモコンの赤外線を学習させようと、 PC-OP-RS1 の受光部に向けて赤外線を発射しても、 PC-OP-RS1 側では何も受け取っていない様子。 10年くらい使ってなかったから赤外線受光素子が劣化してしまったのか?

赤外線の受光はできないものの、 発光は可能みたい。 13年前に書いた Perl スクリプトを使って Nature Remo に向けて赤外線を発射してみると、 ちゃんと Nature Remo で波形データを生成できた。 ということは、 波形データさえ用意できれば今でも使えそう。

ただし、 13年前に PC-OP-RS1 を買ったときは、 波形データのフォーマットを知らなくても使えたので、 単に PC-OP-RS1 が出力した波形データを 16進数の羅列として perl スクリプトに取り込んだだけ。 当時書いた「日記」からスクリプト (の冒頭部分) を引用:

#!/usr/bin/perl
use strict;
use warnings;
use Device::SerialPort;
use Getopt::Std;

my %Ir;
$Ir{'vPower'} = [
    pack("H*", "ffffffffffffffffffffff0700000000007ef0831ff8c00f7e00003f00800ffc00003f00801f00c00700f00300f8c10f7c00003f00801f00e00700f0831f00c00f7ee0033ff8c10ffc00003ff00100fc00007e00001f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
    ];
$Ir{'aPower'} = [
    pack("H*", "ffffffff0f00000080ff000000fc030000f01fc07f000000fe01fc070000e03f000000ff01fe030000f01f000080ff00fe010000f80fe03f000000ff000000fc07f01fc03f000000ff01fc07f80fe03f807f00ff01fc03f80f0000c07f00ff00fe03f80fe01fc07f00ffffffff07000000c03f000000ff010000f80fe01f000000ff00fe030000f01f0000807f00ff010000f80f0000c03f80ff000000fc07f00f0000c07f000000fe03f807f01f000080ff00fe01fc07f00fe03f80ff00fe01fc070000e03f807f00ff01fc03f80fe03f80ffffffff01000000f01f0000000000000000000000000000000000feffff"),
    ];

 ...以下略 ...

スクリプト中 「vPower」 はビデオテープレコーダ (VTR) の電源をオン/オフする赤外線のデータ。 「aPower」 は (おそらく) エアコンのオン/オフ。 後に続く 16進数の羅列が赤外線の波形データ。 どちらの家電もすでに無く (VTR なんてすでに死語?)、 そのリモコンも捨ててしまった。 なのでこのスクリプトが (今でも) ちゃんと機能するかは確認のすべがない。

とりあえず vPower の 16進数を 2進数で表示してみる:

senri:~ $ perl -e 'print unpack("b*", pack("H*", "ffffffffffffffffffffff0700000000007ef0831ff8c00f7e00003f00800ffc00003f00801f00c00700f00300f8c10f7c00003f00801f00e00700f0831f00c00f7ee0033ff8c10ffc00003ff00100fc00007e00001f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"))."\n"'
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000001111110000011111100000111111000000111110000001111110000011111100000000000000000111111000000000000000001111100000011111100000000000000001111110000000000000000011111100000000000000000111110000000000000000011111100000000000000000111111000001111110000001111100000000000000000111111000000000000000001111110000000000000000111111000000000000000001111110000011111100000000000000000111111000001111110000001111100000011111100000111111000001111110000001111110000000000000000111111000000111110000000000000000011111100000000000000000111111000000000000000001111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

おお、 (なんとなく ^^;) 赤外線の波形データっぽい。 2進数で表示すると、 最初の 91個の「1」と続く 46個の「0」の連続を除けば、 「1」は 5〜6個続くのに対し、 「0」は 5〜6個か、15〜17個続く。 これは赤外線リモコンの通信フォーマットにおける 1T (5〜6個) および 3T (15〜17個) の区間に対応するのだろう。 ということは T (変調単位) は 2進数 5.5個くらいに対応する、 つまり 2進数 1個は 100μ秒くらいなのだろう。

ちなみに unpack("B*", ...) (descending bit order) も試してみたのだが、 「"b*"」(ascending bit order) のほうが赤外線の波形データっぽかったので、 「"b*"」と仮定して作業を進めた。 とりあえず 2進数に変換してみる、みたいな試行錯誤を 1行スクリプトでサクっと書けてしまえるのは perl ならでは。 さいきんあまり人気がない perl だが、 この手の試行錯誤をするときには今でも一番ではなかろうか?

いっぽう Nature Remo の赤外線波形データはこんな感じ:

senri:~ $ curl -i -X GET "http://Remo-XXXXXX.local/messages" -H "Accept: application/json" -H "X-Requested-With: curl" -H "Expect: "
HTTP/1.0 200 OK
Server: Remo/1.0.77-g808448c
Content-Type: application/json

{"format":"us","freq":37,"data":[3357,1717,385,1305,377,468,386,460,386,460,387,459,383,463,385,462,382,463,383,463,383,462,384,465,381,465,379,1306,386,463,382,460,385,465,381,467,381,459,385,462,384,462,377,1312,386,1309,384,459,387,462,382,460,386,460,385,461,385,462,385,1306,385,461,384,1307,384,461,386,1306,402,1291,399,1291,382,1307,388,456,404,1290,401,443,403,1287,407,440,402,445,401,443,386,462,400,445,384,463,382,464,381,464,384,1306,401,1292,383,1302,409,1285,402,1289,383,1309,383,1306,387,1304,379,1312,407,1285,402,443,405,441,386,460,403,443,385,460,404,442,385,464,403,440,404,1289,405,1285,404,1282,388,1304,409,1282,390,1301,406,441,386,1306,407,436,410,441,401,440,406,443,385,1304,407,1284,404,1287,405,441,386,1305,408,1287,404,1285,385,1304,407,441,383,462,389,40199,3376,1697,384,1306,407,441,402,442,407,440,384,465,400,443,405,442,383,460,385,464,384,461,401,445,402,441,404,1291,400,446,384,461,400,446,381,463,383,466,399,444,402,444,383,1304,407,1287,401,444,384,460,404,442,407,439,406,442,403,441,404,1287,386,460,403,1289,400,445,403,1287,407,1282,409,1285,385,1307,400,444,405,1285,404,442,404,1289,402,442,405,441,401,444,386,459,404,442,405,441,403,443,404,442,407,1286,405,1288,398,1286,406,1285,409,1283,404,1287,407,1286,401,1294,406,1284,401,1288,401,446,381,462,402,445,401,444,402,444,384,462,383,466,398,442,407,1286,401,1288,403,1288,404,1288,403,1288,401,1290,407,439,404,1284,406,441,404,441,409,436,408,439,407,1285,406,1285,403,1289,400,446,402,1288,403,1287,405,1284,404,1286,409,437,406,444,404,39760,3379,1697,402,1288,404,442,405,441,401,444,407,439,406,441,401,446,402,441,407,439,402,445,406,441,401,443,401,1290,405,437,404,446,405,441,402,441,402,442,406,441,403,441,404,1290,402,1290,400,446,404,439,407,439,406,439,404,442,404,442,406,1288,401,442,402,1290,385,462,401,1289,401,1290,404,1288,399,1288,404,441,386,1306,402,446,402,1287,403,446,401,442,402,444,401,445,404,441,402,444,402,444,402,446,400,1290,398,1290,402,1287,385,1309,404,1287,400,1289,403,1292,401,1283,390,1302,387,1304,404,445,384,460,408,436,405,442,385,462,402,443,404,441,404,442,385,1306,404,1287,402,1292,383,1307,401,1289,404,1290,400,443,405,1282,388,461,406,439,404,446,384,461,383,1303,404,1289,385,1303,405,442,404,1288,405,1286,404,1287,402,1290,403,442,406,440,405]}

Nature Remo に向けて赤外線を発射した後、 http でアクセスすれば JSON 形式で波形データを返してくれる。 で、この波形データの意味は? と思う間もなく答が見つかってしまった。 つまんない。

data配列の各要素は、赤外線ONの期間、OFFの期間、ONの期間、OFFの期間、、、、を表している。 厳密には、これは38kHzの変調をデコードしたあとの結果である。実際にはONの期間は38kHzの変調信号になっている。

ぱっと見 400前後の数値が多いなぁと思ったが、 1T 区間に対応するわけね、納得。 ざっと見た感じ 「赤外線ONの期間」 のほうが 「OFFの期間」 より短めになっている感じがしたので、 前者は 85 で割り算し、 後者は 115 で割り算してみた。 この「商」(割り算した結果) の個数だけ 2進数の 1 と 0 を並べ、 16進数に変換すればオシマイ。

Nature Remo 形式から PC-OP-RS1 形式への変換スクリプト:

#!/usr/bin/perl
use strict;
use warnings;

my @data = (3357,1717,385,1305,377,468,386,460,386,460,387,459,383,463,385,462,382,463,383,463,383,462,384,465,381,465,379,1306,386,463,382,460,385,465,381,467,381,459,385,462,384,462,377,1312,386,1309,384,459,387,462,382,460,386,460,385,461,385,462,385,1306,385,461,384,1307,384,461,386,1306,402,1291,399,1291,382,1307,388,456,404,1290,401,443,403,1287,407,440,402,445,401,443,386,462,400,445,384,463,382,464,381,464,384,1306,401,1292,383,1302,409,1285,402,1289,383,1309,383,1306,387,1304,379,1312,407,1285,402,443,405,441,386,460,403,443,385,460,404,442,385,464,403,440,404,1289,405,1285,404,1282,388,1304,409,1282,390,1301,406,441,386,1306,407,436,410,441,401,440,406,443,385,1304,407,1284,404,1287,405,441,386,1305,408,1287,404,1285,385,1304,407,441,383,462,389,40199,3376,1697,384,1306,407,441,402,442,407,440,384,465,400,443,405,442,383,460,385,464,384,461,401,445,402,441,404,1291,400,446,384,461,400,446,381,463,383,466,399,444,402,444,383,1304,407,1287,401,444,384,460,404,442,407,439,406,442,403,441,404,1287,386,460,403,1289,400,445,403,1287,407,1282,409,1285,385,1307,400,444,405,1285,404,442,404,1289,402,442,405,441,401,444,386,459,404,442,405,441,403,443,404,442,407,1286,405,1288,398,1286,406,1285,409,1283,404,1287,407,1286,401,1294,406,1284,401,1288,401,446,381,462,402,445,401,444,402,444,384,462,383,466,398,442,407,1286,401,1288,403,1288,404,1288,403,1288,401,1290,407,439,404,1284,406,441,404,441,409,436,408,439,407,1285,406,1285,403,1289,400,446,402,1288,403,1287,405,1284,404,1286,409,437,406,444,404,39760,3379,1697,402,1288,404,442,405,441,401,444,407,439,406,441,401,446,402,441,407,439,402,445,406,441,401,443,401,1290,405,437,404,446,405,441,402,441,402,442,406,441,403,441,404,1290,402,1290,400,446,404,439,407,439,406,439,404,442,404,442,406,1288,401,442,402,1290,385,462,401,1289,401,1290,404,1288,399,1288,404,441,386,1306,402,446,402,1287,403,446,401,442,402,444,401,445,404,441,402,444,402,444,402,446,400,1290,398,1290,402,1287,385,1309,404,1287,400,1289,403,1292,401,1283,390,1302,387,1304,404,445,384,460,408,436,405,442,385,462,402,443,404,441,404,442,385,1306,404,1287,402,1292,383,1307,401,1289,404,1290,400,443,405,1282,388,461,406,439,404,446,384,461,383,1303,404,1289,385,1303,405,442,404,1288,405,1286,404,1287,402,1290,403,442,406,440,405);
my $str = "";
my $bit = 1;
for my $d (@data) {
    if ($bit) {
        $str .= $bit x ($d / 85);
        $bit = 0;
    } else {
        $str .= $bit x ($d / 115);
        $bit = 1;
    }
}
$str = unpack("H*", pack("b*", $str)). "\n";
print "$str\n";

1行スクリプトに書けなくもないが、 まあ無理に 1行にしなくても、 このくらいならソッコーで書ける。 やっぱり perl が一番 :-)。

実行してみると ↓ こんな感じ。 波形データを PC-OP-RS1 形式に変換して初めて気付いたが、 80個以上の 0 が連なる区間 (2進数だと 320個以上、つまり 32ミリ秒以上の空白) があり、 3つの波形データに分けられることが分かる。

senri:~ $ ./irconv.pl
ffffffff7f00e001f0f0f0f07878787878787878003c3c3c3c3c1e1e1e000f80c7c3c3c3c3c303e0e101f0f00078003c001e008f07c0e301f0783c1e1e0f0f0f0f8007c003e001f00078003c001e000f8007c0e3f1f078783c3c1e000f8007c003e001f000783c001e8fc7e301f00078003c1e000f8007c003e0f1f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0ffffffff0f003c001e8fc7c3e3f1f0f0f0783c001e0f8f8787c7e301f000783c3c1e8fc703e0e101f078003c001e000f80c703e0f100783c1e8fc7e3f178003c001e000f8007c003e001f00078003c001e0f8fc7e3e1e1f10078003c001e000f8007c0e301f0783c1e0f8007c003e0f10078003c001e008fc703000000000000000000000000000000000000000000000000000000000000000000000000000000000000f8ffffffff03000f80c7e3f1783c1e8fc7e3f100783c1e8fc7e3f10078003c1e8fc7e3f100783c001e1e000f8007c003e0f100783c001e8fc7e3f1783c1e000f8007c003e001f00078003c001e000f80c7c3e3f1f0783c1e000f8007c003e001f000783c001e1e8f8707c003e001f078003c001e000f80c7e301

というわけで、 上記「変換スクリプト」をちょこっと書き直して、 赤外線信号が表現しているデータを表示するようにしてみる。 NECフォーマットでも、 家製協(AEHA, 家電製品協会)フォーマットでも、 赤外線OFFの期間が 1T のとき「0」で、 3T のとき「1」だから、 赤外線ONの期間は無視して、 赤外線OFFの期間が 1000以上の時は 1 で、以下なら 0、 そして 3000以上なら信号の切れ目。

Nature Remo 形式から家製協(AEHA)フォーマットへの変換スクリプト:

#!/usr/bin/perl
use strict;
use warnings;

my @data = (3357,1717,385,1305,377,468,386,460,386,460,387,459,383,463,385,462,382,463,383,463,383,462,384,465,381,465,379,1306,386,463,382,460,385,465,381,467,381,459,385,462,384,462,377,1312,386,1309,384,459,387,462,382,460,386,460,385,461,385,462,385,1306,385,461,384,1307,384,461,386,1306,402,1291,399,1291,382,1307,388,456,404,1290,401,443,403,1287,407,440,402,445,401,443,386,462,400,445,384,463,382,464,381,464,384,1306,401,1292,383,1302,409,1285,402,1289,383,1309,383,1306,387,1304,379,1312,407,1285,402,443,405,441,386,460,403,443,385,460,404,442,385,464,403,440,404,1289,405,1285,404,1282,388,1304,409,1282,390,1301,406,441,386,1306,407,436,410,441,401,440,406,443,385,1304,407,1284,404,1287,405,441,386,1305,408,1287,404,1285,385,1304,407,441,383,462,389,40199,3376,1697,384,1306,407,441,402,442,407,440,384,465,400,443,405,442,383,460,385,464,384,461,401,445,402,441,404,1291,400,446,384,461,400,446,381,463,383,466,399,444,402,444,383,1304,407,1287,401,444,384,460,404,442,407,439,406,442,403,441,404,1287,386,460,403,1289,400,445,403,1287,407,1282,409,1285,385,1307,400,444,405,1285,404,442,404,1289,402,442,405,441,401,444,386,459,404,442,405,441,403,443,404,442,407,1286,405,1288,398,1286,406,1285,409,1283,404,1287,407,1286,401,1294,406,1284,401,1288,401,446,381,462,402,445,401,444,402,444,384,462,383,466,398,442,407,1286,401,1288,403,1288,404,1288,403,1288,401,1290,407,439,404,1284,406,441,404,441,409,436,408,439,407,1285,406,1285,403,1289,400,446,402,1288,403,1287,405,1284,404,1286,409,437,406,444,404,39760,3379,1697,402,1288,404,442,405,441,401,444,407,439,406,441,401,446,402,441,407,439,402,445,406,441,401,443,401,1290,405,437,404,446,405,441,402,441,402,442,406,441,403,441,404,1290,402,1290,400,446,404,439,407,439,406,439,404,442,404,442,406,1288,401,442,402,1290,385,462,401,1289,401,1290,404,1288,399,1288,404,441,386,1306,402,446,402,1287,403,446,401,442,402,444,401,445,404,441,402,444,402,444,402,446,400,1290,398,1290,402,1287,385,1309,404,1287,400,1289,403,1292,401,1283,390,1302,387,1304,404,445,384,460,408,436,405,442,385,462,402,443,404,441,404,442,385,1306,404,1287,402,1292,383,1307,401,1289,404,1290,400,443,405,1282,388,461,406,439,404,446,384,461,383,1303,404,1289,385,1303,405,442,404,1288,405,1286,404,1287,402,1290,403,442,406,440,405);
my $str = "";
my $bit = 1;
my $skip = 2;
for my $d (@data) {
    next if $skip-- > 0;
    if ($bit) {
        $bit = 0;
    } else {
        if ($d > 3000) {
            print unpack("h*", pack("b*", $str)). "\n";
            $str = "";
            $skip = 2;
        } elsif ($d > 1000) {
            $str .= "1";
        } else {
            $str .= "0";
        }
        $bit = 1;
    }
}
print unpack("h*", pack("b*", $str)). "\n";

実行結果を以下に示す。 3つの波形は同じデータ 「10010305fa00ff30cf2cd3」(低 nybble が先) の繰り返しだった。 前掲した PC-OP-RS1 形式への変換スクリプトで得た波形データは 454バイトもあったが、 3つの波形が同じなら最初の 1波形 124バイトだけでよいことになる。 PC-OP-RS1 は一度に送ることができる赤外線データが 240バイトという制限があるので、 1波形のみ送ることにした。

senri:~ $ ./iraeha.pl
10010305fa00ff30cf2cd3
10010305fa00ff30cf2cd3
10010305fa00ff30cf2cd3

以下は、 PC-OP-RS1 で赤外線の送信を行うスクリプト。 -d オプションで PC-OP-RS1 のデバイスを指定する。 受光部分が壊れてしまったので、赤外線を学習する機能はない。 前述したような方法 (Nature Remo 等の学習リモコンで元データを生成して変換) で赤外線の波形データを作成し、 連想配列 %Ir に設定する。

緊張しながらこのスクリプトを実行 「./pc-op-rs1 -d /dev/PC-OP-RS1 off」 すると...
みごと 日立LED照明器具 LEC-AHS810K が消灯した。 ということは日立製作所のメーカ識別コードが 0x1001 ってこと? どこかに家製協のメーカ識別コード (カスタマーコード) の一覧って無いだろうか? ちなみに「全灯」ボタンは「10010305fa00ff20df2cd3」だった。

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use Device::SerialPort;

my %Ir;
$Ir{'on'} = pack("H480", "ffffffff7f00e001f0f0783c1e8fc7e3f1783c001e1e8fc7e3f178003c001e1e8fc7e3f100783c001e1e000f8007c003e0e101f0f00078783c1e8fc7e3f10078003c001e000f8007c003e001f078003c1e0f8fc7c303e0e101f00078003c001e000f80c703e0e1e1f1f00078003c001e1e000f8007c003e0e1f1");
$Ir{'off'} = pack("H480", "ffffffff7f00e001f0f0f0f07878787878787878003c3c3c3c3c1e1e1e000f80c7c3c3c3c3c303e0e101f0f00078003c001e008f07c0e301f0783c1e1e0f0f0f0f8007c003e001f00078003c001e000f8007c0e3f1f078783c3c1e000f8007c003e001f000783c001e8fc7e301f00078003c1e000f8007c003e0f1f0");

our ($opt_v, $opt_d, $opt_c);
getopts("vd:c:") || help();
defined $opt_d || die "option -d is needed\n";

my $port = new Device::SerialPort($opt_d) || help();
$port->user_msg(1);
$port->error_msg(1);
$port->baudrate(115200);
$port->databits(8);
$port->parity("none");
$port->stopbits(1);
$port->handshake("none");
$port->read_const_time(100); # 0.1 sec
$port->read_char_time(5);
send_ir($port, "\x69");
recv_ir($port, 1, 3);

my $ch = 1;
if ($opt_c) {
    if ($opt_c =~ /^[1-4]$/) {
        $ch = $opt_c;
    } else {
        help();
    }
}

while ($_ = shift @ARGV) {
    defined $Ir{$_} || help();
    send_ir($port, "\x74")
        && recv_ir($port, 1, 3) eq "\x59"
        && send_ir($port, pack("C", 0x30+$ch))
        && recv_ir($port, 1, 3) eq "\x59"
        && send_ir($port, $Ir{$_})
        && recv_ir($port, 1, 3) eq "\x45"
        && next;
    die;
}
$port->close;
exit 0;


sub send_ir {
    my ($port, $data) = @_;
    $port->write($data);
    print STDERR "send: ", unpack("H*", $data), "\n" if $opt_v;
}

sub recv_ir {
    my ($port, $len, $timeout) = @_;
    my $i = 0;
    my $j = 0;
    my $data;
    while ($i < $len) {
        my ($l, $d) = $port->read(1);
        if ($l > 0) {
            $data .= $d;
            $i += $l;
            $j = 0;
        } else {
            $j++;
            if ($timeout > 0 && $j > $timeout) {
                print STDERR "TIMEOUT to read $len byte\n";
                return "";
            }
        }
    }
    print STDERR "recv: ", unpack("H*", $data), "\n" if $opt_v;
    return $data;
}

sub help {
    print STDERR <<EOF;
Usage: pc-op-rs1 [opt] <com>...
opt:   -d <dev>   device (MUST)
       -c <ch>    channel (1..4)
       -v         verbose
EOF
    print "com: ", join(" ", sort keys %Ir), "\n";
    exit 1;
}
2019年11月28日

IFTTT に登録できないのでお蔵入りになってた Eco Plugs RC-028W & CT-065W が、UDP パケットを送るだけでコントロールできた!

IoT 機器の多くが、 専用のスマホアプリだけでなく Googleアシスタントや Amazonアレクサからコントロールできる。 しかし、 いちいち音声でコントロールするのはメンドクサイ (なぜ音声以外の方法でもコントロールできるようにしないのか?)。 出かけるときに毎回 「行ってきま〜す」 などと Googleアシスタントに呼び掛けるのは、 いかがなものかと思う。 外出を勝手に検知して家電を適切にコントロール (例えば電気ポットの電源を切る) してくれるほうがずっといい。

IoT 機器を IFTTT に登録すると、 自前のプログラムからコントロールできるようになる。 IoT 機器は Googleアシスタントでコントロールするより、 自前のプログラムでコントロールするに限る。 例えば自宅の Wi-Fi LAN にスマホが繋がっているかプログラムで監視し、 繋がってるスマホがいなくなったら留守になったと判断して、 自動的に電気ポットの電源を切れば、 電気ポットのコンセントを抜いたかどうか出先で心配せずに済む。 あるいはコンセントを抜くのを忘れて寝てしまい、 翌朝電気ポットのお湯が熱いままなのを見て愕然とするより (先月の電気使用量が 600kWh だったので驚いた)、 部屋が暗いときは自動的に電源が切れている方がいい (これはプログラムを書かなくても IFTTT だけで実現できる)。

というわけで持ってる IoT 機器を片っ端から IFTTT に登録したのだけど、 IFTTT に登録できない IoT 機器も残念ながら若干ある。 いまどき IFTTT に登録できない IoT 機器に何の意味があるのだろう? (今なら絶対に買わない) と思うのだけど、 IFTTT の便利さを知る前に買ってしまったのだから後悔先に立たず。 IFTTT の便利さを知ってからは、 お蔵入りになっていた。

EcoPlugs RC-028W

Eco Plugs もそんな「使えない」IoT 機器の一つ。 当時としては安価だった (今ならもっと安い) ので Walmart で購入してしまった。 Googleアシスタントや Amazonアレクサには登録できるのに、 肝心の IFTTT に登録できない。

といって通信プロトコルを解析しようにも、 いまどきの IoT 機器はクラウド (ベンダが運用するサーバ) と https で通信するので調べる取っ掛かりがない。 最後の手段、 分解するしかないのか?

ところがググっていると、 Eco Plugs は平文で通信しているという投稿を見つけた。 Eco Plugs はクラウドに登録しなくても、 同一 LAN セグメントならスマホアプリでコントロールできるが、 同一 LAN 内では平文の UDP パケットを飛ばしているらしい。

ありがたいことに Eco Plugs をコントロールする JavaScript プログラムが GitHub に公開されていた。 JavaScript は文法もロクに知らない (^^; のだけど、 見よう見まねで perl で書き直してみる:

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use IO::Socket::INET;
our ($opt_v);
(getopts('v') && @ARGV == 3) || &help;
my ($ip, $id, $on) = @ARGV;

my $state = 0x0100;
$state = 0x0101 if $on eq "on";
my $buf = pack("H260", 0);
# Byte 0:3 - Command 0x16000500 = Write, 0x17000500 = Read
substr($buf, 0, 4) = pack("N", 0x16000500);
# Byte 4:7 - Command sequence num - looks random
substr($buf, 4, 4) = pack("N", rand(0xffffffff));
# Byte 8:9 - Not sure what this field is - 0x0200 = Write, 0x0000 = Read
substr($buf, 8, 2) = pack("n", 0x0200);
# Byte 16:31 - ECO Plugs ID ASCII Encoded - <ECO-xxxxxxxx>
substr($buf, 16, 16) = $id;
# Byte 116:119 - The current epoch time in Little Endian
substr($buf, 116, 4) = pack("L", time());
# Byte 124:127 - Not sure what this field is - this value works, but i've seen others 0xCDB8422A
substr($buf, 124, 4) = pack("N", 0xCDB8422A);
# Byte 128:129 - Power state (only for writes)
substr($buf, 128, 2) = pack("n", $state);

my $sock = IO::Socket::INET->new(PeerAddr => $ip, PeerPort => 80,
    Proto => 'udp', Timeout => 1) || die;
my $flags;
print unpack("H*", $buf) . "\n" if $opt_v;
print $sock $buf;
$sock->recv($buf, 1024, $flags);
print unpack("H*", $buf) . "\n" if $opt_v;

# Byte 10:14 - ASCII encoded FW Version - Set in readback only?
my $fwver = substr($buf, 10, 5);
# Byte 48:79 - ECO Plugs name as set in app
my $name = substr($buf, 48, 32);
$name =~ s/\0*$//;
printf("%s (ver %s)\n", $name, $fwver);

sub help {
    print <<EOF;
Usage: ecoplugs <opt> <IP> <ID> <on/off>
opt:   -v           ; verbose
EOF
    exit 1;
}

長さ 130 バイトの UDP パケット (変数 $buf) を作って Eco Plugs へ送信している (print $sock $buf;) だけなので、 いたってシンプル。 ユーザ認証もないので LAN 内なら誰でもコントロールできる。

Eco Plugs の IP アドレス (第1引数) と、 Eco Plugs の ID 「ECO-XXXXXXXX」(第2引数, XXXXXXXX は MACアドレスの第3〜6オクテット, ただし 16進数の A〜F は大文字限定)、 および「on」あるいは「off」の 3引数を付けて、 この perl プログラムを実行すると、 該当 Eco Plugs をオン/オフし、 Eco Plugs の名前 (スマホアプリで設定できる。以下の例では 「potplug」) と、 ファームウェアのバージョン (以下の例では 「1.6.3」) を表示する。

senri:~ $ ecoplugs -v 192.168.15.123 ECO-01234567 on
16000500940c163b020000000000000045434f2d303132333435363700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a625df5d00000000cdb8422a0101
160005000000163b0000312e362e330045434f2d30313233343536370000000000000000000000000000000000000000706f74706c7567000000000000000000000000000000000000000000000000003031323334353637000000000000000000000000000000000000000000000000a8e23b7ea625df5d00000000cdb8422a
potplug (ver 1.6.3)

「-v」オプションを付けた場合、 最初に表示される行が Eco Plugs へ送った長さ 130バイトの UDP パケット (260桁の 16進数)、 2行目が Eco Plugs から返ってきた長さ 128バイトの UDP パケット (256桁の 16進数)。 第1引数で指定した IP アドレスが Eco Plugs のものでなかった場合、 あるいは第2引数で指定した ID が間違っている場合など、 応答が返ってこない時は待ち続ける。 ID の 16進数において A〜F が小文字だと応答しないので注意。

Eco PlugsRC-028W (屋外用) および CT-065W (屋内用) で動作を確認したが、 おそらく同シリーズの他の機器でも使えるだろう。 Woods の WiON (スマホアプリが Eco Plugs そっくり) でも使えるらしい。

More...
2019年11月19日

IoT な人感センサをトリガーとした照明の点灯/消灯を IFTTT を使って行っていたけど反応が遅いので IFTTT をショートカットしてみた

さいきん流行りの IoT 機器。 多くの家電がネットからコントロールできるようになった。 IFTTT を使うと、 そういった機器を手軽に連携できるので便利。 IoT 機器同士だけでなく、 (私が管理する) WWW サーバを IFTTT がアクセスするように設定したり、 あるいは逆に私のサーバが IFTTT をアクセスする (トリガーを送る) こともできるので、 思いのままに IoT 機器を制御できる。

例えば、 人感センサで照明を点灯/消灯させる場合、 防犯用ライトなら人の動きを感知したときだけ点灯し、 人の動きが無くなれば速やかに消灯する、 といった単純なルールで充分だが、 部屋の照明となると人の動きが無くなったからと言ってすぐに消されては困る。 部屋を退出したことを確認してから消灯して欲しいし、 時間帯、あるいは在宅/不在時に応じて (さらにはその時々の天気に応じて)、 適切な点灯/消灯制御を行いたい。

つまり、 部屋の外にも人感センサを設置し、 部屋の中で人の動きが検知できなくなった後、 部屋の外で人の動きを検知すれば、 部屋を出ていったと判断し部屋の照明を消灯する。 さらに、 特定のスマホが LAN (家庭内 Wi-Fi) に接続していないときは不在とみなし、 部屋の外で人の動きを検知しただけでは照明を点灯しないけど、 そのスマホが LAN に接続した直後は帰宅したとみなし、 夜間であれば人の動きを検知したら速やかに点灯するなど。 私自身のサーバ (以下、「自サーバ」と略記) を IFTTT と連携させれば、 いくらでも複雑な制御ルールを設定できる。

IFTTT (IF This Then That) は、 その名の通り特定の条件 (This) が満たされたとき特定の動作 (That) を行わせることができる。 IoT 機器の多くは IFTTT との連携をサポートしているので、 例えば「This」として、 「人感センサが人の動きを検知」を設定し、 「That」として、 「照明をオン」を設定すれば、 単純な防犯用ライトが実現できる。

この連携に自サーバを絡めるには、 「This」および「That」を自サーバと結び付ければ良い。 それには IFTTT の 「webhooks」を用いる。

「This」は、 IFTTT の特定の URL をアクセスするだけ。 例えばこんな感じ:

senri:~ $ curl https://maker.ifttt.com/trigger/light_on/with/key/dD-v7GCx46LnWaF1AD9nwSUeA_N1ALvDHKS57cP1_Md
Congratulations! You've fired the light_on event

「light_on」の部分は任意に定めることができる。 「with/key/」以降の部分はユーザごとに IFTTT が割当てる認証用キー。 このキーが他人に漏れると勝手に操作されてしまうので適切な管理が必要。 そして、 「https://maker.ifttt.com/trigger/light_on/with/key/... へのアクセスがあった」(This) ならば、 「照明をオン」(That) を行う、 というルールを設定することで、 自サーバから照明を点灯させることが可能になる。

いっぽう 「That」 は、 IFTTT に自サーバをアクセスさせる。 例えば 「https://www.gcd.org/ifttt へ POST メソッドでアクセス」させる。 POST の body として json データを送るよう設定することができて、

{"magic": "0svikYKbcsxDbkty", "type": "Motion detected", "CreatedAt": "{{CreatedAt}}", "DeviceName": "{{DeviceName}}"}

などと設定する。 「"magic": "0svikYKbcsxDbkty"」は認証用。 https://www.gcd.org/ifttt は誰でもアクセスできるので、 "magic" の文字列が一致しないリクエストは無視する。 「"type"」は 「This」の機器の種類 (この例では人感センサ) を伝えるために設定。 「{{CreatedAt}}」と 「{{DeviceName}}」は、 「This」の機器が IFTTT へ送信したデータ。 例えば人感センサが検知 (This) すると IFTTT が次のようなアクセスを www.gcd.org へ行ってくれる (That)。

POST /ifttt HTTP/1.1
Content-type: application/json
host: www.gcd.org
content-length: 134
x-newrelic-id: ZW1uPtmAO9tRDSFGGvmp
x-newrelic-transaction: VGhpcyBpcyBmYWtlIHgtbmV3cmVsaWMtdHJhbnNhY3Rpb24uCg==
Connection: close

{"magic": "0svikYKbcsxDbkty",
 "type": "Motion detected",
 "CreatedAt": "November 19, 2019 at 09:15AM",
 "DeviceName": "廊下センサ"
}

この IFTTT からのアクセスを受信することで、 人感センサが人の動きを検知したことを自サーバが知ることができる。 そして自サーバにおいて様々な条件を加味した後、 前述した 「https://maker.ifttt.com/trigger/light_on/with/key/...」 へアクセスすれば照明を点灯することができる。

以上で、 IoT の連携に自サーバを絡ませることができるようになった。 ところがこの方法は、いかんせん遅い。 人感センサ ⇒ IFTTT ⇒ 自サーバ ⇒ IFTTT ⇒ 照明 などと IFTTT とのやりとりを 2度も行うため、 人の動きを検知してから照明が点灯するまで 6秒ほどかかってしまう。 部屋に入るまで 6秒も待てないので、 暗いままの部屋に入る羽目になる。 なお、 点灯するのは素早さが肝要だが、 消灯するのは数秒程度の遅れなら全く問題にならない。

More...
Filed under: システム構築・運用,ハードウェアの認識と制御 — hiroaki_sengoku @ 15:58
2019年9月4日

UEFI ブートでキーボードが無いと GRUB がハングするバグを修正してみた hatena_b

遅ればせながら手元の PC を MBR ブートから UEFI ブートに切り替えた。 ハードディスクの最初の 512バイト MBR (Master Boot Record) を読み込んで起動するのが MBR ブート。 Windows だと 2TB 超のディスクは MBR ブートできない (Linux ならブート可能)。 2TB では手狭になってきたのが切り替えを決意した理由だが、 ついでに Linux 専用マシンも PC のファームウェアが対応しているものは UEFI ブートに切り替えた。

UEFI (Unified Extensible Firmware Interface) だと、 普通の FAT32 ボリュームにブートローダのファイルを置いておくだけなので、 わざわざ MBR を書き換えたりするより簡単だし分かりやすい。 PC が起動しなくなった、などのトラブルはよくあるが、 トラブル発生時は気が急くし時間的余裕がないことも多いので、 トラブル時の作業は簡単であればあるほど、 分かりやすければ分かりやすいほど好ましい。

UEFI ブートに切り替えて 1ヶ月ほど経ったある日、 CPU を換装するために落としていた PC の電源を入れたら GRUB のメニュー画面でフリーズしたので私も凍り付いた。 CPU の性能をむやみに上げると、マザーボードとのミスマッチが起りがち。 せっかく買った新しい CPU が問題を起こしたのかと思った。 電源ボタンを長押しして強制的に電源を落とす。

BIOS 設定を確認するためにキーボードをつないで再度電源を入れてみる。 設定に何の問題もない。 続いて GRUB を立ち上げる。問題無く立ち上がる。 Linux を起動する。問題無い。 さっきのフリーズは何だったのだろう?

この時は因果関係に気付かなかったが、 キーボードをつないでいないと GRUB 2.04 (および最新版の 2.05 も) がメニュー画面のカウントダウンでハングする (最初の秒を表示したまま止まる)。 PC を再起動するときは、たいていキーボードをつないでいるので正常に起動し、 この症状は見たことがなかった。

実はこの時も、 なぜキーボードをつないでいなかったかというと明確な理由はない。 CPU 換装後の動作確認なのだからキーボードをつないでおくべきだと思うのだけど、 たまたまつなぐのを忘れていただけかも。 Web で 「headless GRUB hang bug UEFI without keyboard」 などを検索しても似た症例がほとんど見つからないのは、 キーボードをつながずに起動させる人がほとんどいないから?

とはいえ、 何台もあるサーバそれぞれにキーボードをつないでいては邪魔である。 意図して再起動するときはキーボードをつなぐなり KVM スイッチ (Keyboards, Video monitors, Mice Switch) を切り替えてキーボードが効く状態にするのが通常だが、 トラブルや Watchdog タイマーで勝手に再起動したとき、 あるいは遠隔から再起動させたときに、 立ち上がらずにフリーズしてしまったら非常に困る。 不測の再起動に備えて、 急遽テンキーをつないでおくことにした。

GRUB がキー入力を検知する状態であれば、 画面表示の有無自体は関係なくハングする。 逆に言うと、 GRUB がキー入力を検知しなければ、 例えば grub.cfg でキー入力を読む命令が無ければ (menuentry 等が無ければ) ハングしない。 例えば grub.cfg が次のように特定の Linux を起動するだけなら、 正常に起動できる。

insmod all_video
insmod part_gpt
insmod search_label
search --no-floppy --label --set=root /
linux /boot/linuz-4.19.69-x86_64 root=LABEL=/ resume=LABEL=swap ro
initrd /boot/initz-4.19.69-x86_64
boot

もちろんこれでは GRUB を使う意味がないが、 少なくとも問題が GRUB のキー入力関連にあることが分かった。 同じ PC、同じ GRUB でも、 UEFI ブートではなく MBR ブートなら、 キーボードをつながなくてもハングしない (MBR ブート専用の USB メモリを作って確認した。末尾の「おまけ」参照)。 また、UEFI ブートであっても UEFI ファームウェアによってはハングしない場合もあると思われる (未確認)。 が、少なくとも私の PRIMERGY MX130 S2 では確実に再現する。

というわけで、 問題の所在はおおむね絞り込めたので、 GRUB のソースコードを読み始めた。 並行して facebook に、 ことの顛末を書込んだ

すると野中尚道さんから grub-core/tem/efi/console.c が怪しそう、 とのヒントを頂いた。 ありがたい! なにぶん EFI のソースコードを読むのは初めてなので、 この時点ではまだ流れを追いきれていなかった。 efi/console.c の grub_console_getkey まわりを重点的に読んでみる。

grub-core/tem/efi/console.cのget_keyで key_exとkey_conを呼び分けている処理が失敗しているような気がします。 key_exの方はオプション機能なので実装有無を確認してるのですが

なるほど確かに grub_console_getkey_ex は、 grub_efi_open_protocol の返り値が NULL で無いとき (key_ex の実装が有るとき) に限り呼び出されているが、 grub_console_getkey_con には対応するものがない。 key_con は必ず実装されているから確認不要? でも、キーボードをつないでいない場合はどうなる? key_ex の有無を確認するコード:

text_input_ex_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID;
        ...
text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
                                    &text_input_ex_guid,
                                    GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

をマネして、 key_con の有無を確認するコードをでっち上げてみる。 単に 「_EX_」 の部分を取り除いただけだが、 キーボードをつないでいるか否かを正しく検出しているようだ。

text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;
text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
                                    &text_input_guid,
                                    GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

あとはキーボードの有無に応じて grub_efi_console_input_init の返り値を変えるだけ。 grub_efi_console_input_init が 1 (非ゼロ?) を返せば、 GRUB は grub_console_getkey を呼ばなくなるようだ。

以上をまとめると、 次のようなパッチになった:

diff --git a/grub-core/term/efi/console.c b/grub-core/term/efi/console.c
index 4840cc5..f2be32f 100644
--- a/grub-core/term/efi/console.c
+++ b/grub-core/term/efi/console.c
@@ -207,7 +207,12 @@ grub_efi_console_input_init (struct grub_term_input *term)
                                       GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
   term->data = (void *)text_input;
 
-  return 0;
+  if (text_input) return 0;
+  grub_efi_guid_t text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;
+  text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
+                                      &text_input_guid,
+                                      GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+  return text_input == NULL;
 }
 
 static int

わずか 6行だが、 これで GRUB がハングしなくなった。

More...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 21:21
2019年8月26日

Windows 10 上の VMware Workstation Player のゲストOS でタグVLAN を使ってみる

Windows 上の仮想化ソフトウェアの定番 VMware Workstation Player で、 タグVLAN (tagged VLAN, IEEE 802.1Q) を使うことはできないと今まで思い込んでいたが、 レジストリエディタ (regedt32.exe) でレコードを一つ追加するだけで使えるようになった。 ググっても Windows 上のゲストOS でタグVLAN を使う話は見かけないのでメモしておく。

私の自宅内LAN はタグVLAN を利用している。 つまり物理的な配線は一本のイーサケーブルで、 複数のネットワーク (宅内LAN, DMZ, 対外セグメントなど) を同居させている。 タグVLAN はオフィス等で使われることが多いが、 美観上の理由からケーブルを何本も這わすわけにはいかない家庭内LAN においてこそ、 タグVLAN は有用と思う。

Linux OS などタグVLAN 対応の OS が走るマシンへは、 タグが付いたままのパケットを流し、 Windows OS などタグVLAN に非対応な OS が走るマシンへは、 スマートスイッチでタグを取り除いた (通常の) パケットを流している。

ところが、 マシンによっては Linux と Windows の両方の OS を走らせることがある (いわゆるデュアルブート)。 OS を切り替えるたびにスマートスイッチの設定を変更するのは面倒なので、 そういうマシンには Linux と Windows の両方がアクセスするネットワーク (つまり自宅内LAN) のパケットはタグ無しで流しつつ、 Linux のみがアクセスするネットワーク (DMZ や対外セグメント) のパケットはタグ付で流すことになる。

Windows が立ち上がってるときもタグ付パケットが届くが無視される。 Linux が立ち上がっているときはタグ付、タグ無し両方のパケットを受け取る。

さいきんは PC のメモリも大きくなり、 普段は Windows を使うマシンでも、 VMware Workstation Player (以下 VMware と略記) などの仮想化ソフトウェアを使って Linux をゲストOS として走らせておくことが増えてきた。 デュアルブートよりも同時に走らせておくほうが便利に決まってる。 となってくると、 ゲストOS でもタグVLAN を使いたくなるのが人情というもの。

ゲストOS でタグVLAN が扱えると、 本来 DMZ へ置くべきようなサーバ (WWW サーバとか) をゲストOS 上で動かすことが可能になる。 また、 ゲストOS でも自宅内LAN とインターネットとの両方のネットワークへアクセスできるわけで、 ルータの役割を担わせてもよい。 いずれにしても仮想マシンの応用範囲が一気に広がる。

しかし VMware を使って起動した仮想マシン上では、 (仮想)ネットワークインタフェースにタグ付パケットが上がってこないので、 ゲストOS でタグ付パケットを拾うことができない。 VMware がタグVLAN に対応していないのだろうと諦めていた。 まあタダで利用させてもらってるソフトウェアだし、 対応してなくても仕方がないなぁと。

ところが、 実は VMware 自体はタグVLAN に対応しているらしい。 Windows 10 がタグ付パケットを落としているから、 VMware までパケットが届かないということらしい。 細かく言えば Windows 10 が悪いというよりは、 ネットワークインターフェース (以下 NIC と略記) のドライバがタグ付パケットを落としているらしい。

私が使ってる Windows 10 では NIC が 「Broadcom NetLink (TM) Gigabit Ethernet」 と表示されるので、 「windows10 broadcom capture vlan」 あたりのキーワードで検索していたら、 Windows 上でタグ付パケットをキャプチャする方法について書かれたページを見つけた。

なお google では同じキーワードを使って検索しても、 このページを見つけ出すことはできなかった。 ニッチなものを見つけたい場合に、 google 検索が全く役に立たなくなったのは、 google がモバイル検索にシフトし始めた頃だったろうか? 探しているページが全く見つけられないので、 最近は google 検索を使わなくなってしまった。 google お得意の AI で賢く検索するより、 バカ正直にキーワード検索してくれたほうが (少なくとも私にとっては) 役に立つ。 Stay Foolish !

この見つけたページには、 Windows 上のパケットキャプチャソフトウェア Wireshark でタグ付パケットを見る方法が書かれている:

Display VLAN tags in Wireshark on laptops with Broadcom B57 chipsets から引用:

In order to make these tags visible to Wireshark, specialized drivers or specific NICs that support VLAN tags are usually needed. In the case of the Broadcom B57 chipset in some Dell Latitude laptops, the NIC itself supports VLAN tags (display only, it cannot actively tag outgoing traffic) with a small registry modification and a specific driver.

(意訳) Wireshark でタグ付パケットを見るには、 NIC ドライバがタグVLAN に対応している必要がある。 Broadcom B57 チップセット自体はタグVLAN に対応しているので、 ドライバのレジストリをいじればタグ付パケットを見ることができるようになる。 ただし見るだけでタグ付パケットを送信することはできない。

このページには 「送信できない」 と書いてあるが、 後述するようにゲストOS からタグ付パケットを送信できている。 Wireshark と VMware とでは違うのかも?

レジストリをいじるには、 まず NIC ドライバのインスタンスを見つけなければならない。 このページでは 「TxCoalescingTicks」 を検索せよと説いている。 Broadcom B57 チップセットのドライバのインスタンスであれば、 この名前のレコードを必ず持っているからだろう。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{クラスID}\番号 を見つけたら、 このインスタンスに 「PreserveVlanInfoInRxPacket」 という名前で文字列値 「1」 を追加すればよい。

ちなみに、 「tx coalesce ticks」 を WWW で検索すると Broadcom 関連のページばかりが出てくる。 Broadcom 製 NIC 特有の機能なのかも? Linux の BCM5700 用ドライバ にも tx_coalesce_ticks というパラメータがあるようだ。

というわけで、 Broadcom 製 NIC でタグ付パケットを落とさないようにする方法は以下のようになる:

VLAN capture setup から引用:

  1. Run the Registry Editor (regedt32).
  2. Search for "TxCoalescingTicks" and ensure this is the only instance that you have.
  3. Right-click on the instance number (eg. 0008) and add a new string value.
  4. Enter "PreserveVlanInfoInRxPacket" and give it the value "1".

意訳:

  1. レジストリエディタ regedt32 を起動する
  2. 「TxCoalescingTicks」 を検索する。 見つけたインスタンスが (Backup 用のインスタンスを除いて) 唯一であることを確認する。
  3. 見つけたインスタンス番号 (例えば 0008) を右クリックし、 「新規 文字列値」 を追加する。
  4. 「PreserveVlanInfoInRxPacket」 を入力し、 その値を 「1」 とする。

Intel 製 NIC の場合も、 ドライバのレジストリをいじることで、 タグ付パケットを落とさないようにできるらしい。

レジストリをいじった後は、 再起動すれば NIC ドライバがタグ付パケットを捨てずに受け取るようになっている。 ドライバが捨てなければ、 タグ付パケットは Windows OS から VMware に渡され、 ゲストOS まで届く。 逆にゲストOS から発せられたタグ付パケットは、 Windows OS から NIC へ送られ、 ネットワークへ送出される。 つまりゲストOS がタグVLAN を扱えるようになった。

Filed under: システム構築・運用 — hiroaki_sengoku @ 09:06
2018年4月12日

NETGEAR のスマートスイッチ GS108E/GS116E のポートごとの通信量を取得して Cacti でグラフ化してみた 〜 通信量を見ることができない Wi-Fi中継機への対処法 〜

たまたま買った Wi-Fiアクセスポイント (以下 Wi-Fi AP と略記) が、 SNMP 非対応なのはもちろん、 Web管理画面でさえ通信量を見ることができなかったので、 この Wi-Fi AP を接続しているスイッチ (ネットワークハブ) NETGEAR GS108E (と GS116E) のポートの通信量を取得して Cacti (最近の流行りは Grafana ?) で可視化してみた。 Wi-Fi そのものの通信量ではないが、 Wi-Fi AP とスイッチとの間の通信量を測ることができる。

買ったのは NETGEAR WiFi中継機/802.11ac wave2 Nighthawk X4 EX7300-100JPS (以下 EX7300 と略記)。 たまたま amazon で税込5,980円 (送料無料) で売っていて、 中継機 (ワイヤレスエクステンダー) としてだけでなくアクセスポイントとしても使えるとのことなので (NETGEAR製ということもあって) 衝動買い。 もともとルータの機能は必要ないし、 コンセント直挿しで場所を取らないので、 リビング用としてちょうどいいかなと思った次第。

さいきん Wi-Fi中継機が流行りらしく、 各社からお手頃価格で多数の製品が発売されている。 コンセント直挿しで場所を取らないので家庭向きと言えるが、 残念なことにアクセスポイントとして使える製品は (NETGEAR や TP-Link などのごく一部の例外を除くと) ほとんど無い。
中継機も (広義の) アクセスポイントと言えるが、 親機 (Wi-Fiルータ) と無線でつながるのが中継機で、 有線でつながるのが (狭義の) アクセスポイント。 あたりまえだが、 有線でつなぐことができる (家庭内でケーブルを配線できる) なら、 有線のほうがいいに決まってる。
ちなみに私が初めて Wi-Fi中継機を使ったのは 10年前なので、 いまさら流行って少々戸惑いを覚える。

EX7300 は、 たった 6000円なのに性能的には悪くない。 ルータ機能が不要ならお買得と思う。 ノートPC Lavie HZ を Wi-Fi で EX7300 につないで iperf3 を走らせて測定してみたら 530Mbps くらい出ている。

C:\Users\sengoku>c:\bin\iperf3.exe -c esaka -P 5
Connecting to host esaka, port 5201
[  4] local 192.168.18.147 port 50119 connected to 192.168.18.20 port 5201
[  6] local 192.168.18.147 port 50120 connected to 192.168.18.20 port 5201
[  8] local 192.168.18.147 port 50121 connected to 192.168.18.20 port 5201
[ 10] local 192.168.18.147 port 50122 connected to 192.168.18.20 port 5201
[ 12] local 192.168.18.147 port 50123 connected to 192.168.18.20 port 5201
[ ID] Interval           Transfer     Bandwidth
[  4]   0.00-1.00   sec  13.4 MBytes   112 Mbits/sec
[  6]   0.00-1.00   sec  13.2 MBytes   111 Mbits/sec
        ...

[SUM]   9.00-10.01  sec  63.9 MBytes   533 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth
[  4]   0.00-10.01  sec   130 MBytes   109 Mbits/sec                  sender
[  4]   0.00-10.01  sec   130 MBytes   109 Mbits/sec                  receiver
[  6]   0.00-10.01  sec   128 MBytes   108 Mbits/sec                  sender
[  6]   0.00-10.01  sec   128 MBytes   108 Mbits/sec                  receiver
[  8]   0.00-10.01  sec   128 MBytes   107 Mbits/sec                  sender
[  8]   0.00-10.01  sec   128 MBytes   107 Mbits/sec                  receiver
[ 10]   0.00-10.01  sec   126 MBytes   106 Mbits/sec                  sender
[ 10]   0.00-10.01  sec   126 MBytes   106 Mbits/sec                  receiver
[ 12]   0.00-10.01  sec   124 MBytes   104 Mbits/sec                  sender
[ 12]   0.00-10.01  sec   124 MBytes   104 Mbits/sec                  receiver
[SUM]   0.00-10.01  sec   636 MBytes   534 Mbits/sec                  sender
[SUM]   0.00-10.01  sec   636 MBytes   534 Mbits/sec                  receiver

iperf Done.

たまたま手元にあった TP-Link WiFi 無線LAN ルーター Archer C3150 11ac にも Wi-Fi でつないで同様に iperf3 を走らせて比較してみたら、 ほとんど差がない。 同等と言ってもいいレベル。 値段は 3倍くらい違うのだから、 EX7300 をブッチぎって欲しかった。

しかもこの Archer C3150 は OpenVPN を使ってると腐るバグがあるように感じる (いちど電源を切らないと復旧しない)。 ヘンテコな付加機能よりも安定性こそ一番重要なのではないか? ついでに言うと勝手に外部と通信しているのが気持ち悪い。 値段が高い Wi-Fiルータにはもうちょっと頑張ってもらいたいところ。

もちろん、 このノートPC を無線LAN ではなく有線LAN につなげば 940Mbps くらいは出るので、 ノートPC の性能がボトルネックになっているわけではない。 というか Wi-Fi のパフォーマンスは周囲の電波状況に強く影響され (もちろん空いてるチャンネルを使っている)、 有線側の状況はほとんど関係がない。 電波暗室とかで測定すれば、 EX7300 と Archer C3150 の差が開くのかもしれないが、 実用上は無意味。

性能でも安定性でも申し分のない EX7300 ではあるが、 残念なことに Web管理画面を隅から隅まで調べてみても通信量を見る方法がない。 どんな安物 Wi-Fiルータ (or Wi-Fi AP) でも、 通信量 (パケット数とか) を表示させる方法はあるものだと思っていたが、 中継機は Wi-Fiルータとは製品コンセプトが違うのかも?

通信量を見ることができない通信機器があるとは思っていなかったので、 意表を突かれた感じ。 今後 Wi-Fi AP を買う際は注意したい。 やっぱり (ルータ機能が必要なくても) 素直に Wi-Fiルータを買うべきなのかも。 ほんとうは (ルータ機能がない) アクセスポイント専用機が欲しいのだけど...

幸い、 この EX7300 を接続しているスイッチ NETGEAR GS108E が「スマート」で、 スイッチの各ポートごとの通信量 (≒ Wi-Fi AP の通信量) を GS108E/GS116E 付属の Windows アプリ ProSafe Plus で見ることができる。

ProSafePlus GS116E Port Statistics

Web アプリではなく Windows アプリなので、 表示された通信量の数値を Web スクレイピングなどのお手軽な方法で取り込むことはできないが、 google で検索したら ProSafe Plus と同等のことができる Python プログラムが見つかった。 6年前に開発が終了しているが、 このスイッチを買ったのも 6年前なので無問題。

さっそくダウンロードして (ざっと内容を確認したのち) 走らせてみる:

senri $ git clone https://github.com/Z3po/Netgearizer.git ↵
Cloning into 'Netgearizer'...
remote: Counting objects: 88, done.        
remote: Total 88 (delta 0), reused 0 (delta 0), pack-reused 88        
Unpacking objects: 100% (88/88), done.
Checking connectivity... done.
senri $ Netgearizer/netgearizer.py ↵
please select one of the following switches with "selectSwitch $NR"
--> 0: 192.168.18.239
Information: 
switch-name: living
switch-dhcp: disabled
switch-type: GS108Ev2
switch-ip: 192.168.18.239
switch-mac: 4c:60:de:69:01:23
switch-firmware: 1.00.06
switch-netmask: 255.255.255.0
switch-gateway: 192.168.18.1
(Cmd) 

スイッチは 3つ (GS116E と GS108E 2台) あるのに、 なぜか 1つ (リビングに設置した GS108E) しか表示されない。 selectSwitch コマンドでスイッチを選択し (選択肢は 1つしか無いが)、 getPortStatistics コマンドでポートごとの通信量を表示できるようだ。

実行してみる:

(Cmd) selectSwitch 0 ↵
(Cmd) getPortStatistics ↵
switch-port-statistics: 
 -> Port 01: 
   >> send: 2664610542955
   >> receive: 5993649462240
   >> crcerrors: 0
 -> Port 02: 
   >> send: 6062498001
   >> receive: 328715447216
   >> crcerrors: 0
 -> Port 03: 
   >> send: 183424114643
   >> receive: 8723285000144
   >> crcerrors: 0
 -> Port 04: 
   >> send: 39935317815
   >> receive: 26550837425456
   >> crcerrors: 0
 -> Port 05: 
   >> send: 137960667631
   >> receive: 790398362288
   >> crcerrors: 0
 -> Port 06: 
   >> send: 3948410207
   >> receive: 1156872131840
   >> crcerrors: 0
 -> Port 07: 
   >> send: 2789623
   >> receive: 20875141728
   >> crcerrors: 0
 -> Port 08: 
   >> send: 1989939088
   >> receive: 2852348890112
   >> crcerrors: 0
(Cmd) 

一見、 正しく動いているように見えるが、 「send:」に表示されている数値は (ProSafe Plus で見たときの) 受信バイト数で、 「receive:」に表示されている数値は送信バイト数を 16倍した値になっている。

受信と送信が入れ替わっているのはご愛敬だが、 16倍になっているのは... netgearizer.py (Python で書かれたスクリプト) を読んでみると、 単純なバグだった。 次のパッチをあてると正常に動作した:

--- netgearizer.py.org        2018-04-07 22:04:42.292912972 +0900
+++ netgearizer.py        2018-04-09 11:34:50.124297214 +0900
@@ -284,5 +284,5 @@
             for port in hexvalue:
-                sendstats = self.__convertFromHex(port[2:18],'cipher')
-                receivestats = self.__convertFromHex(port[19:35],'cipher')
-                crcerrors = self.__convertFromHex(port[36:53],'cipher')
+                receivestats = self.__convertFromHex(port[2:18],'cipher')
+                sendstats = self.__convertFromHex(port[18:34],'cipher')
+                crcerrors = self.__convertFromHex(port[82:98],'cipher')
                 result.append(( 'Port ' + str(port[:2]), (('send', sendstats), ('receive', receivestats), ('crcerrors', crcerrors))))
@@ -345,15 +345,16 @@
                 for key in self.switches.keys():
                     self.switchList.append(key)
                     print '--> ' + str(counter) + ': ' + key
                     print 'Information: '
                     self.selectedSwitch = key
                     self.__printResult(True)
                     self.selectedSwitch = None
-                    return True
+                    counter += 1
+                return True
             else:
                 print 'please select one of the switches you get with getSwitches first.'
                 return False
 
         if 'ERROR' in resultdict:
             found = None
             for key in  self.switchattributes.keys():

送信バイト数が 16倍になるのは、 部分文字列の切り出しで「port[18:34]」 (変数「port」の文字列の 18文字目から 34文字目まで切り出す) とすべきところを、 「port[19:35]」 としてしまっていたため。 変数「port」には、 スイッチから受信したデータを 16進数に変換した文字列が格納されている。 1文字ずれて切り出したため、 16倍 (16進数で 1桁ぶん) になっていた。

16進数で 16文字つまり 64bit だが、 ProSafe Plus で表示される GS116E の送受信バイト数が小さすぎると思っていたら、 GS116E はバイト数が 32bit (4GiB) でラップアラウンドしているようだ。 つまり上位 32bit は常にゼロ。ポートによっては毎日ラップアラウンドしている。
GS108Ev2 は 40bit (1TiB) を超えるバイト数になっているポートもあるので、 どこでラップアラウンドするか確認できるまで、 まだだいぶかかりそう。 仮に 1ヶ月で 1TiB くらいの通信量だとすると、 もし 48bit (256TiB) が上限なら 20年もかかってしまう。
もし 64bit 全部使ってカウントしているなら、 上限は 16EiB (エクスビバイト) ということになり、 140万年くらいかかってしまう。 ちょっとのコスト増を惜しんでラップアラウンドするより、 64bit きっちり使って半永久にラップアラウンドしないほうがいい。

実は、 私は今まで Python でプログラミングしたことが無い。 文法もろくに知らなかったのだけど、 部分文字列を切り出す際の Python の位置の指定方法は独特だと思う。 このプログラムの作者も Python は慣れていなくて、 うっかりこのようなバグを作り込んでしまった (前の行で 18文字目まで切り出したので、 その次は 19文字目からとしたくなる気持ちは分かる) のではないか?

さらに興味深いのが、 3つあるはずのスイッチが 1つしか見つからなかったバグ。 なんと、 「return True」文のインデント (字下げ) が間違っていただけ。 インデントでブロックを表わす Python ならではのバグ?

インデントが一段階深かったので、 「return True」文が forループの中に入ってしまい、 必ずループが 1回だけで終了してしまっていた。 だからスイッチを 1つ見つけてすぐループを終了していた。 「return True」文の前の空白を削除してループの外へ出したら、 正しく 3つのスイッチを見つけるようになった。 空白の有無で挙動が変わってしまうのは、 やっぱり気持ち悪い。

この netgearizer.py は対話的にコマンドを入力するソフトウェアだが、 通信量を自動計測する際には使いにくいので、 対話せず通信量を取得するだけのプログラムに書き換えた。 不要な機能をばっさり削除したので、 プログラムの行数が半分以下の 317行になった。 この改変版は、

http://www.gcd.org/sengoku/docs/netgearizer.py

からダウンロードできる。 わずかな改変とはいえ、 なにぶん私が初めて書く Python プログラムなので、 変なところがあったらご指摘頂けると幸い。

この改変版を実行する (引数としてインタフェースを指定する) と、 以下のように LAN 内 (同一セグメント内) の全ての NETGEAR スマートスイッチ (GS116E, GS108E, GS105E など) の、 各ポートの受信バイト数、送信バイト数、エラー数を表示する。

senri:~ $ netgearizer.py eth0 ↵
IP:   192.168.18.239
MAC:  4c:60:de:69:01:23
Type: GS108Ev2
switch-port-statistics: 
 Port01 rx:2673873720246 tx:375671471563 err:0
 Port02 rx:6410023533 tx:21992185424 err:0
 Port03 rx:183424114643 tx:545205312509 err:0
 Port04 rx:40052812544 tx:1664344574414 err:1
 Port05 rx:138616484625 tx:52251007903 err:0
 Port06 rx:3954090302 tx:72431873899 err:0
 Port07 rx:2802623 tx:1308888305 err:0
 Port08 rx:1989939088 tx:178271805632 err:0
IP:   192.168.18.237
MAC:  4c:60:de:69:04:56
Type: GS108Ev2
switch-port-statistics: 
 Port01 rx:786331851 tx:1283347 err:0
 Port02 rx:0 tx:0 err:0
 Port03 rx:0 tx:0 err:0
 Port04 rx:0 tx:0 err:0
 Port05 rx:0 tx:0 err:0
 Port06 rx:474556 tx:875905 err:0
 Port07 rx:0 tx:0 err:0
 Port08 rx:0 tx:0 err:0
IP:   192.168.18.236
MAC:  84:1b:5e:90:81:72
Type: GS116E
switch-port-statistics: 
 Port01 rx:2245190893 tx:3192491053 err:0
 Port02 rx:2079058908 tx:4030809209 err:0
 Port03 rx:0 tx:0 err:0
 Port04 rx:2815311290 tx:1592352771 err:0
 Port05 rx:3838533539 tx:2573311206 err:0
 Port06 rx:1441584273 tx:1249733039 err:0
 Port07 rx:747795050 tx:97733030 err:0
 Port08 rx:889171012 tx:559797414 err:0
 Port09 rx:3757714660 tx:749158734 err:0
 Port0a rx:3210386057 tx:2724670950 err:0
 Port0b rx:441710237 tx:2183781562 err:0
 Port0c rx:0 tx:0 err:0
 Port0d rx:4837745 tx:252496639 err:0
 Port0e rx:2058650283 tx:3610878767 err:0
 Port0f rx:1435416601 tx:403439849 err:0
 Port10 rx:0 tx:0 err:0
senri:~ $ 

私の自宅の LAN ではタグVLAN を使っているため、 以上のコマンドを実行している Linux マシン「senri」の eth0 インタフェースには IP アドレスを割当てていないが、 問題なく動作している。 しかも、 この Linux マシンは GS116E の Port04 に接続していて、 2台の GS108E には GS116E 経由で間接的に繋がっているのだけど、 GS108E の通信量も取得できている。 Windows アプリの ProSafe Plus だと、 スイッチに直接繋げないと管理できないことがある。

改変版 netgearizer.py で取得した送受信バイト数を Cacti (最近の流行りは Prometheus とか Fluentd ?) に読み込ませて EX7300 の通信量をグラフ化すると、 こんな感じ:

NETGEAR EX7300 Cacti

In がスイッチのポートの送信バイト数、 つまりスイッチから EX7300 が受信したバイト数、 すなわち Wi-Fi 子機 (スマホなど) がダウンロードした合計。

Out がスイッチのポートの受信バイト数、 つまりスイッチへ EX7300 が送信したバイト数、 すなわち Wi-Fi 子機がアップロードした合計。

実は ProSafe Plus のもう一つの Python 実装である ProSafeLinux も試してみたのだが、 IP アドレスを割当てていないインタフェースを受付けないので、 先に netgearizer.py の書き換えを試みた次第。

ProSafeLinux のほうが (active ではないにせよ) 今でも開発が続いているし、 前述したようなバグも無いようなので、 試してみるべきだとは思うが、 プログラムの行数が netgearizer.py の倍以上 (私が作った改変版の 5倍以上) もある。 通信量の取得といった単純な目的であれば、 短い方がいい。

Filed under: システム構築・運用 — hiroaki_sengoku @ 10:10
2018年3月8日

配当金領収証の上限は 100万円、複数枚でも簡易書留で送付できるらしい 〜 祝 KLab(株) 初配当 (特別配当 9円) 〜

株式の配当金って、 (特に手続きしなければ) 郵便為替 (正確に言うと、 株式会社ゆうちょ銀行が発行する配当金領収証) を利用することがほとんどだと思いますが、 金額が大きくなっても郵便為替を使うのかなぁ? と日頃からギモンに思っていました。

「領収証」って名称ですが、為替証書の一種です。 株主届出印を押印して郵便局へ持っていくと換金できます (身分証明書の提示が必要)。

そんなある日、 KLab株式会社から 「定時株主総会招集ご通知」 と題する簡易書留郵便が届きました。

「ご通知」 を簡易書留で?
不審に思いつつ受け取って開封すると...
3枚の配当金領収証が出てきました。(@_@)

KLab Dividend

「配当金領収証」の上限って 100万円なんですね、初めて知りました。 配当金の額面は 255万円余り (税引き前は 320万円余り) なのに、 簡易書留 (5万円が上限) で送れるとは、 ちょっとビックリ。

送金できる金額は、 配当金領収証1通につき100万円までです。 100万円を超える送金は、 配当金領収証を発行することで行えますが、 追加する配当金領収証の枚数に応じた料金がかかります。

もっと金額が大きくなると、どうなるんだろう?

その企業の役職員の場合や、法人株主宛の場合は、 さすがに郵便為替ということはないと思うのですが、 個人株主宛の場合は、 どんなに高額でも (事前に手続きしない限り) 郵便為替を使うんですかねぇ?

- o -

18年前の 2000年7月末、 (株)ケイ・ラボラトリー (当時の社名) への出資金として 300万円を振り込みました。

シティバンク銀行 (現 SMBC信託銀行PRESTIA) 虎ノ門支店で、 虎の子の 300万円をおろしたものの、 目と鼻の先にある東京三菱銀行 (現 三菱東京UFJ銀行) 虎ノ門支店まで現金を持っていくのが怖くて (引ったくりに遭ったらどうしよう?)、 840円もの振込手数料を払って振り込んだのが思い出されます。

Citibank JPY3M

1年後にどうなってるか分からない会社 (実際、かなり危うい時期が何度かありました) にポンと 300万円もの大金を投じた当時の私 (← 株式会社日立製作所から転職したばかりの 33歳でお金持ってません) もビックリですが、 それから 18年たって、 出資した額以上の配当金 (税引き前 320万円余り) を出せる会社に成長したのもビックリです。

今回の配当金の総額は 3億3500万円だそうですが、 創業当時の資本金が 3億円でした。

正確に言うと創業時の私の出資分に対する今回の配当金は 162万円 (税引き後は 129万円ほど) です。 創業時 1株 5万円だったので、 私が出資した 300万円は 60株になったのですが、 2004年10月30日に 1株が 2株に分割、 2011年4月21日に 1株が 300株に分割、 2012年1月31日に 1株が 5株に分割しました。 つまり、 創業時の 1株が 3000株になったので、 私の 60株は現在の 18万株になったわけです。 今回の特別配当は 1株あたり 9円ですから、 9円 * 18万株=162万円ですね。

もっとも、 創業当時とは全く異なる事業の会社になってしまいましたが、 全く異なる会社に脱皮できたからこそ今の成長があるのでしょう。

初の配当おめでとうございます。 そして、 ありがとうございました。 株主の一人として、また創業者の一人として御礼申し上げます。

Filed under: 元CTO の日記 — hiroaki_sengoku @ 23:04
2017年8月11日

ZenFone3 で撮影した写真ファイル内の、位置情報が壊れている Exif データを修復するコードを書いてみた

昨年発表された ASUS のハイエンド スマホ ZenFone 3 Deluxe ZS570KL の購入時の OS は Android 6.0 Marshmallow だが、 この OS (UL-Z016-WW-4.12.40.1698) の標準のカメラ アプリには、 撮影した写真の位置情報が他のアプリで参照できないというバグがあった。

つまり、 カメラの設定で 「場所サービス」 を 「オン」 にすると、 撮影した場所 (GPS などの位置情報) を写真ファイルに付加する (Exif データ) が、 この位置情報に問題があって、 他のアプリで写真を見ても位置情報が表示されない。 例えば撮影した写真を Google フォトへアップロードしても、 撮影場所が表示されない

browsed with Gallery Application

この写真を Google フォトからダウンロードしてみれば、 Exif データ内に位置情報が保持されていることが分かるし、 アプリによってはこの位置情報を参照できるものもある。

例えば OS 標準の 「ギャラリー」 アプリで見ると、 位置情報が正しく保持されているように見える。 例えば、 御成町の交差点で撮影したこの写真を 「ギャラリー」 で見ると (右図 ⇒ ZenFone のスクリーンショット)、 場所が 「鎌倉市, 御成町」 と正しく表示される。

位置情報を正しく表示できるアプリがある以上、 バグではなく仕様 (Exif のバージョンの違い?) の可能性もある。 このとき私は、 この写真の位置情報を参照できないアプリは、 アプリ側にも問題があるのだろうと思ってしまった。 おそらく開発元でも同様の理由で、 このバグが見逃されてしまったのだろう。

More...
Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 22:09
2017年7月26日

カナダで Project Fi を使ってみた 〜 現地でプリペイドSIMを買うより安いし、同時に複数のスマホで使える hatena_b

昨年に続いて今年も、 日本の梅雨 (と夏の暑さ) から逃れるべく バンクーバーに滞在した昨年の滞在では fido, WIND, Virgin のプリペイド SIM を買ったが、 今年 1月に米国を訪れたとき Project Fi を契約したので、 Project Fi の SIM をバンクーバーでも使ってみた。

Project Fi は米国外で使っても (米国内で使う場合と同じく) データ通信が $10/GB (1MB あたり 1セント) という、 リーズナブルかつシンプルな料金体系。 カナダだと、 何故かキャリアのプリペイド SIM を買うより安くつく。 カナダでは Telus や Bell の通信設備を使ってローミングサービスしているようだが、 Telus や Bell のプリペイド SIM よりも料金が安いのは何故なのか?

バンクーバーのいたるところにキャリアの店舗があるので、 適当に入ってみた店で、 「いま Google の Project Fi を使っているのですが、 Project Fi より安くなるプランありますか?」と (もちろん英語で) 聞いてみたら、 「Project Fi みたいに安いプランはありません!」 と即答されてしまった。(^^;

しかも、 Project Fi の大きな特徴として、 追加料金無しで Data-only SIM を 9枚まで追加できる。 妻のスマホに Data-only SIM を入れておけば、 Project Fi 一契約だけで (私と妻、二人分の) 用が足りてしまう。 Data-only SIM を 9枚入手すれば、 10人で共有できる?

なお、 Project Fi の (通話可能な) SIM は、 Pixel や Nexus に入れて使うことが推奨されているが、 ふつうのスマホに入れても通話やデータ通信が可能。 私はバンクーバー滞在中ずっと、 ZenFone 3 Deluxe (ZS570KL) に入れていた。 もちろん Pixel も持ってはいるのだが、 ZenFone のほうがメモリが多く、 また root 権限を取得済なのでいろいろ使い勝手が良い。

もちろん、 いくら安くても通信速度が遅ければ意味がないので、 その時は現地でプリペイド SIM を買うつもりでバンクーバーを訪れたのだが、 Project Fi でもしっかり LTE の電波をつかんでくれた。 昨年使った fido や Virgin のプリペイド SIM と比べてなんら遜色がない (WIND は 3G のみ)。 結局 27日間のバンクーバー滞在で、 一度も現地 SIM を買おうという気にはならなかった。

Project Fi のページで確認すると、 7月の 24日間のデータ通信量が 7.36GB (私が 2.62GB、妻が 4.75GB) だった。 ホテルの Wi-Fi が使い物にならないほど遅かったので、 ホテルに居るときまで Project Fi を使うことになり、 ふだん日本で 1ヶ月に使うデータ通信量より多くなってしまった。

最近のアプリ (特に facebook, twitter, Instagram など) は、 画像や動画をストレスなくどんどん表示するために、 勝手にどんどん通信するのでいかがなものかと思う (私はこの手の通信しまくりアプリは一切インストールしない)。 滞在したホテルは 「無料 Wi-Fi 完備」 をうたっていたが、 宿泊客がこの手の通信しまくりアプリを使うためか、 夕方以降は全くといっていいほど通信できなくなってしまっていた。

日ごとのデータ通信量は次のような感じ:

Project Fi Current cycle

日によってばらつきが大きいが、 平均 300MB くらい。 二人分なのでこんなもの? 7.364GB で $73.64 (約 8250円)。 国際電話の通話料が 21分間で $4.20 (約 470円)。 さらに Project Fi の基本料金 $20 と税金等 ($6 くらい?) がかかる。 まだ請求が来ていない (〆日は 8月2日) が、 合計 $104 くらい?

Project Fi で、 カナダから米国の toll free 番号に電話できるのか? と思いつつ 1-888 から始まる電話番号に電話したら繋がったので、 てっきり無料通話かと思って安心して長電話してしまった。 有料なら繋がらないで欲しい (>_<)。 繋がらなければ、 米国外からかけるときに使えるコレクトコール番号を使ったのに。
1分あたり 20セントという国際電話料金は、 ケータイの通話料の感覚からすると安いが、 Google Voice の感覚からすると高い。 ZenFone 3 に Project Fi の SIM を入れて通話したのだが、 Pixel (あるいは Nexus 5X) に入れて通話すれば無料だったのかも?
少なくともハングアウトで発信していれば無料だった。

昨年のバンクーバー滞在は 14日間と今年の半分の日数だったが、 fido の SIM に CA$89.60 (約 7900円)、 Virgin の SIM に CA$72.79 (約 6400円)、 WIND (現 Freedom Mobile) の SIM に CA$72.80 (約 6400円) 支払った。 もし今回 Project Fi がなくて昨年と同様にプリペイド SIM を買っていたら、 昨年の倍、4万円近くかかってしまったかもしれない。

カナダでは、 どのキャリアも 1GB 程度使おうとすると、CA$70 以上かかってしまう。 各キャリア横並びの料金で、 格安SIM 登場前の日本みたいな感じ。 キャリア間の競争がないのかも? どの月額プランを選ぶかでデータ通信の課金レートが変わってくるので、 最適な月額プランを選ぶのが難しい。

これに対し Project Fi は、 どんなに使っても 1MB あたり 1セントの単一課金レートなので分かりやすいし、 課金レートだけ比較すると、 カナダのどのキャリアよりも安い (例えば比較的安価な Virgin でも 2¢/MB)。

ただし Project Fi はプリペイド SIM ではなくポストペイ SIM なので、 米国に住所がないと契約が難しいかも? 私の場合はホテルの住所で契約することができた。 私は米国の銀行が発行したクレジットカードで払っているが、 日本のクレジットカードで契約できるかどうかは不明。

また、ポストペイなので当然だが、 使わなくても毎月基本料金 ($20 と税金等が $3 ほど) がかかる。 日本でもローミングサービスが利用できるが、 日本だと 1¢/MB という課金レートは (格安 SIM と比べると) 安いとは言えず、 常用には向かない。

日本に居るときは基本料金が無駄になるなぁ、 $23 というのは無駄に払う額としては安くないなぁ、 と思いつつ (今年 1月に) 契約したのだが、 実は Project Fi の契約は一時停止できる。

一時停止中は、 データ通信ができないのはもちろん、 通話の着信もできなくなるが、 いまどき通話なんてできなくても困らない。 Google Voice の番号は他にも持ってるし。 :-)

というわけで今年 3月1日に一時停止した。 停止/再開は Project Fi の Web ページからいつでも可能。 3ヶ月後に自動で再開してしまうが、 その直後に停止すれば基本料金はほとんどかからない。 3ヶ月後の 5月30日に自動再開したが、 6月4日に停止し、 バンクーバーを訪れる直前 6月27日に再開し、 日本に帰国してから停止した。 自動再開した直後に停止することさえ忘れなければ (自動再開を知らせるメールが届くので見落とさなければ大丈夫)、 使っていない期間の基本料金をほぼゼロにできる。

More...
Filed under: Android,SIM — hiroaki_sengoku @ 21:48
2017年4月19日

ZenFone3 Deluxe を Nougat 7.0 にしたらデータ通信できなくなった 〜 アンロックして Android 6 に戻す 〜 hatena_b

Nexus 5X が突然壊れて以来、 私はメインのスマホとして ZenFone 3 Deluxe ZS570KL を使っている。 購入時の OS は Android 6.0 Marshmallow だったが、 今年 3月にシステム更新のお知らせが表示されたので、 更新を行なったら Android 7.0 Nougat WW-5.14.44.1898 になった (3月26日)。

ところが!

この更新によって通話もデータ通信もできなくなってしまった。 LTE ネットワークへ接続できなくなっていて、 アンテナ・ピクトも表示されない (バツ印が付く)。 最初 SIM の接触不良を疑ったのだが、 SIM を入れ直しても接続できないまま。

更新作業は Wi-Fi のあるところ (自宅) で行なうわけで、 (モバイル) データ通信できなくても Wi-Fi でデータ通信できるから気付きにくい。 まさか更新で通話やデータ通信ができなくなってしまっているとは思わないので、 翌日 (3月27日) も気付かずに出かけてしまい、 出先でデータ通信できないことに初めて気付いて往生した。 出かける前に気付いていれば、 昔のスマホ (Nexus 5X とか) に SIM を入れ替えて使うこともできただろうに。

LTE で接続できないなら、 W-CDMA で接続してみてはどうだろう?と思ったので、 設定メニューで 「モバイルネットワーク」 を選び、 「有線ネットワークタイプ」 を 「2G/3G/4G」 から 「2G/3G」 へ変更 (つまり LTE 接続を抑制) してみた。 すると無事 3G (W-CDMA) ネットワークへ接続して (アンテナ・ピクトが表示されて) 通話ができるようになった。 しかし 3G でもデータ通信はできない。

もちろん、 SIM を別のスマホに入れると何の問題もなく通話もデータ通信もできるので、 SIM や設定の問題ではなく、 Nougat な ZS570KL の問題。

この時点では ZS570KL の root 権限を取得していなかった (まだブートローダをアンロックしていなかった) ので、 できることは限られている。 仕方ないので、 設定メニューの 「バックアップとリセット」 で 「データの初期化」 を行なってみると、 LTE へ接続できるようになった (3月28日)。

Android のバージョンを上げるたびに初期化を強要されてはかなわないと思いつつも、 いちおうこれで一件落着なのだが、 せっかく初期化したのだから、 この機会に (自分流にカスタマイズする前に) ブートローダをアンロックすることにした。

昨年 9月11日に発売開始した ZS570KL だが、 アンロックの方法を ASUS が公開したのは半年近くが経過した 2月16日になってから。 アンロックツール ZS570KL_UnlockTool_V1.1.zip が ASUS の Driver & Tools ページからダウンロードできる。

ASUS UnlockTool

アンロックを行なうとデータの初期化が行なわれてしまうので、 アンロックするならカスタマイズする前の方がいい。 本当は購入直後にアンロックするのが一番なのであるが、 当時はアンロックツールが (少なくとも公式には) 公開されていなかったので、 いままでアンロックせずに使い続けてきた次第。

ASUS のページからダウンロードした zip ファイルを展開して得られた UnlockTool-9.0.0.29_ZS570KL.apk を ZS570KL へインストールして実行すると、 警告画面 (→ 右図) が表示される。

アンロック (ロック解除) すると一切の保証が無くなるだけでなく、 ソフトウェアのアップデートも受けることができなくなると書いてある。 もちろん、 同ページから最新のソフトウェアをダウンロードすること自体は (ロック解除とは関係なく) 可能なので、 ここで言う 「ソフトウェアのアップデート」 とは、 あくまで自動で行なわれる更新のことだろう。

「押してデバイスのロック解除を行う」 を押すと、 再起動と初期化が行なわれる。 これでブートローダがアンロックされ、 ボリューム大ボタンを押しながら電源を入れると、 ブートローダ・モードへ入ることができる。

Nexus 等のブートローダ・モードは、 起動する OS を選べたりするが、 ZS570KL のブートローダ・モードは、 通常の起動画面 (ASUS ロゴ) と変わらず、 ブートローダ・モードに入ってるか外見上は区別できないので注意が必要。 ZS570KL と USB ケーブルでつないだ PC 上で fastboot コマンドを実行することで、 任意のブート・イメージ (TWRP など) を起動したり、 フラッシュメモリへ書込んだりすることが可能。

きちんと動作する ZS570KL 用の TWRP がちょうど公開されていた (3月19日, これ以前のバージョンは画面が逆になるなど、いろいろ不具合があった) ので、 ZS570KL をブートローダ・モードにした上で、 fastboot コマンドを使って起動してみる:

$ fastboot boot twrp-3.1.0-0-Z016D-20170319-N.img 
downloading 'boot.img'...
OKAY [  0.777s]
booting...
OKAY [  0.394s]
finished. total time: 1.171s
$ 

ちょっと使ってみた限りでは、 問題無く使えるようだ。 早く TWRP 公式ページからダウンロードできるようになることを望む。 TWRP を ZS570KL の recovery パーティションへ書込んでおくと、 ボリューム小ボタンを押しながら電源を入れることで、 (PC とつながなくても) TWRP を起動できる。

$ fastboot flash recovery twrp-3.1.0-0-Z016D-20170319-N.img 
target reported max download size of 536870912 bytes
sending 'recovery' (19980 KB)...
OKAY [  0.777s]
writing 'recovery'...
OKAY [  0.021s]
finished. total time: 0.797s
$ 

アンロックしたらついでに root 権限も取得したくなるのが人情なので、 SuperSU v2.79 SuperSU-v2.79-201612051815.zip を TWRP からインストールした。

と、 ここまでは順調だったが、 ある日 (4月4日) 出先で唐突にデータ通信できなくなった。 外出中にデータ通信できなくなると往生する (前述したように 3月27日に体験済み) ので、 こんなこともあろうかと ZS570KL には普段使っている IIJmio の nano SIM の他に、 非常用として Aeon Mobile の micro SIM も入れていた。 電話の着信は両方の SIM で可能 (DSDS, Dual SIM Dual Standby) で、 電話の発信とデータ通信は、 どちらの SIM で行なうか選ぶ方式。 データ通信を Aeon Mobile へ切り替えると、 無事データ通信できた。

なぜ Aeon Mobile ではデータ通信できるのに、 IIJmio ではできないのだろう? と思いつつも、 IIJmio でもアンテナ・ピクトは表示されるし、 IIJmio の電話番号に着信できるので、 そのまま使い続けた。

もちろん、 Aeon Mobile なら他の SIM でもデータ通信できるのか?とか、 micro/nano どちらの SIM トレイに入れるかでデータ通信の可否が変わるのか?とか、 いろいろ確認したいことはあり、 その都度いろんな実験をしている。 が、結論から言えばいずれも直接の関係は無さそうだった。 わざわざ他キャリアから Aeon Mobile へ MNP して新たに nano SIM を作ってみたがデータ通信できなかったし、 IIJmio や mineo の nano SIM にゲタを履かせて micro SIM トレイに入れてもデータ通信できるようにはならなかったし、 nano SIM トレイでも例えば香港3 の nano SIM なら (ローミングで) データ通信できた。

そんなある日 (4月13日)、 IIJmio の電話番号へ電話がかかってきたので出たら、 音声が互いに聞こえなかった。 驚いて Aeon Mobile で通話実験したら、 Aeon Mobile でも着信/発信ともに音声が聞こえなくなっていた。

少なくとも 4月11日までは普通に通話できていたので、 2日間の間に何があったか思い出そうとしたが全く心当たりがなかった。 ある日突然、通話できなくなるなんてことが起きると大変困る。

# 4月21日追記: TWRP で DSP をリストアすると発症するらしい。
# 音声が聞こえない症状については Nougat への更新とは無関係だった。

データ通信ができないのはモバイル・ルータを持ち歩くことで対処できるが、 通話ができないのでは話にならない。 直ちに ZS570KL の使用を中止し、 IIJmio SIM を Pixel に入れて Pixel を持ち歩くことにした。 Pixel は Band 19 をサポートしていないので docomo (の MVNO) では使いにくく、 (日本では) あまり使っていなかった。

というわけで ZS570KL の使用を中止したので、 さらに思いきった実験ができるようになった。 手始めに再度初期化してみることにした。 今回は root 権限を取得済なので、 バックアップ/リストアが手軽にできる。 初期化してデータ通信が復活すれば、 徐々に設定を変えていくことで原因の切り分けも可能だろう。

ところが!

初期化したのに IIJmio のデータ通信は復活しなかった (4月14日)。 通話も着信/発信はできるけど音声が伝わらない。 ZS570KL 単体の問題ではなくて、 LTE ネットワークとの相性も関係あるのか? ますます何が原因なのか分からなくなってしまった。

初期化ではなく Nougat をクリーン・インストールすれば、 通話もデータ通信も元通りできるようになるのかもしれない。 しかしいつ同じ症状が再発しないとも限らない。 突然データ通信できなくなったり、 音声が聞こえなくなったりするようでは安心して使えない。 しかもこの Nougat にはメニューボタンが使えないという (私にとっては) 重大なバグもある (本来は 「ASUS カスタマイズ設定」 でマルチタスクボタンの長押しでメニューを表示するように設定できるはずだが、 この Nougat では機能しない)。

通話やデータ通信ができなくなるのはおそらくバグだろうし、 メニューボタンのほうは明らかにバグ。 ついでに言うと、 カメラが EXIF に記録する位置情報が壊れていて、 他のソフト (Googleフォトを含む) で位置情報が認識できないというバグもある。 そのうち修正版が公開されるのだろうと思っていたが、 Android 7 Nougat WW-5.14.44.1898 が公開されてから一ヶ月がたつのに何も発表されない。 ASUS はバグを認識していないのか?

# 4月19日追記: WW-5.14.44.2096 が公開された。が、症状は変わらず (>_<)

事ここに至っては Nougat の使用を諦め、 Android 6 Marshmallow に戻すしかないと決断した。 といっても、 バージョンを戻す方法は (ASUS 公式には) 提供されていない。 が、 アンロックした ZS570KL であれば、 フラッシュメモリに直接書込めるので、 Marshmallow のイメージを書込めばよい。 つまりクリーン・インストール。

このブログを書いていて (いまごろ orz) 気付いたが、 上記 TWRP のダウンロードページ に、 「Downgrade_to_4.12.40.1698」 というディレクトリがあった。 Android 6 に戻したいという (私と同様な) 人が多いのだろう。

まず ASUS の Driver & Tools ページから Marshmallow の最後のバージョン WW-4.12.40.1698 の zip アーカイブをダウンロード。これを展開する:

senri:~/ZS570KL $ unzip UL-Z016-WW-4.12.40.1698-user.zip
Archive:  UL-Z016-WW-4.12.40.1698-user.zip
signed by SignApk
 extracting: system.patch.dat        
  inflating: META-INF/com/android/metadata  
  inflating: META-INF/com/google/android/update-binary  
  inflating: META-INF/com/google/android/updater-script  
  inflating: boot.img                
  inflating: file_contexts           
  inflating: firmware-update/NON-HLOS.bin  
  inflating: firmware-update/adspso.bin  
  inflating: firmware-update/cmnlib.mbn  
  inflating: firmware-update/cmnlib64.mbn  
  inflating: firmware-update/devcfg.mbn  
  inflating: firmware-update/emmc_appsboot.mbn  
  inflating: firmware-update/hyp.mbn  
  inflating: firmware-update/keymaster.mbn  
  inflating: firmware-update/mdtp.img  
  inflating: firmware-update/pmic.elf  
  inflating: firmware-update/rpm.mbn  
  inflating: firmware-update/tz.mbn  
  inflating: firmware-update/xbl.elf  
  inflating: logs/version            
  inflating: system.new.dat          
  inflating: system.transfer.list    
  inflating: META-INF/com/android/otacert  
  inflating: META-INF/MANIFEST.MF    
  inflating: META-INF/CERT.SF        
  inflating: META-INF/CERT.RSA       
senri:~/ZS570KL $ 

zip アーカイブの中の boot.img が android のブート・イメージ。 このファイルの中に、 Linux カーネルと initramfs が含まれている。 これを boot パーティションへ書込む:

senri:~/ZS570KL $ fastboot flash boot boot.img
target reported max download size of 536870912 bytes
sending 'boot' (13305 KB)...
OKAY [  0.524s]
writing 'boot'...
OKAY [  0.021s]
finished. total time: 0.545s
senri:~/ZS570KL $ 

zip アーカイブの中の firmware-update ディレクトリ下の 13個のファイルが、 ブートローダ等のファームウェア。

拡張子が *.bin のファイル (2個) はファイルシステムのイメージ。 NON-HLOS.bin は /firmware ディレクトリに、 adspso.bin は /dsp ディレクトリに、 それぞれ読み取り専用でマウントされる。 どちらも AMSS モデム関連のディレクトリ。 NON-HLOS.bin は modem パーティションへ、 adspso.bin は dsp パーティションへ、 それぞれ書込む:

senri:~/ZS570KL $ fastboot flash modem firmware-update/NON-HLOS.bin
target reported max download size of 536870912 bytes
sending 'modem' (81916 KB)...
OKAY [  3.166s]
writing 'modem'...
OKAY [  1.895s]
finished. total time: 5.060s
senri:~/ZS570KL $ fastboot flash dsp firmware-update/adspso.bin
target reported max download size of 536870912 bytes
sending 'dsp' (16384 KB)...
OKAY [  0.652s]
writing 'dsp'...
OKAY [  0.401s]
finished. total time: 1.053s
senri:~/ZS570KL $ 

拡張子 *.mbn は multi-boot binary ファイル (8個) で、 ARM の ELF (Executable and Linkable Format) オブジェクト。 keymaster.mbn と cmnlib.mbn は 32bit の共通ライブラリで、 keymaster は証明書の暗号鍵を管理する。 cmnlib64.mbn は 64bit の共通ライブラリ。

rpm.mbn と emmc_appsboot.mbn は 32bit ARM の ELF 実行ファイル。 rpm は 「resources & power manager」 を意味し、 プライマリ・ブートローダの役割を果たす。 emmc_appsboot はセカンダリ・ブートローダで、 このブートローダが android のカーネルを起動する。

残りの devcfg.mbn, hyp.mbn, tz.mbn は 64bit ARM の ELF 実行ファイル。 機能の詳細は分からないが、 devcfg は device config、 hyp は hypervisor、 tz は trust zone のことだと思われる。

詳細不明なファイルが多いが (^^; 以上 8個の *.mbn ファイルを、 それぞれ対応するパーティションへ (emmc_appsboot.mbn は aboot パーティションへ、 それ以外はファイル名と同じ名称のパーティションへ) 書込む:

senri:~/ZS570KL $ fastboot flash cmnlib firmware-update/cmnlib.mbn
target reported max download size of 536870912 bytes
sending 'cmnlib' (200 KB)...
OKAY [  0.029s]
writing 'cmnlib'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.060s
senri:~/ZS570KL $ fastboot flash cmnlib64 firmware-update/cmnlib64.mbn
target reported max download size of 536870912 bytes
sending 'cmnlib64' (254 KB)...
OKAY [  0.030s]
writing 'cmnlib64'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.061s
senri:~/ZS570KL $ fastboot flash devcfg firmware-update/devcfg.mbn
target reported max download size of 536870912 bytes
sending 'devcfg' (39 KB)...
OKAY [  0.040s]
writing 'devcfg'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.070s
senri:~/ZS570KL $ fastboot flash aboot firmware-update/emmc_appsboot.mbn
target reported max download size of 536870912 bytes
sending 'aboot' (749 KB)...
OKAY [  0.047s]
writing 'aboot'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.077s
senri:~/ZS570KL $ fastboot flash hyp firmware-update/hyp.mbn
target reported max download size of 536870912 bytes
sending 'hyp' (257 KB)...
OKAY [  0.038s]
writing 'hyp'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.069s
senri:~/ZS570KL $ fastboot flash keymaster firmware-update/keymaster.mbn
target reported max download size of 536870912 bytes
sending 'keymaster' (220 KB)...
OKAY [  0.032s]
writing 'keymaster'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.062s
senri:~/ZS570KL $ fastboot flash rpm firmware-update/rpm.mbn
target reported max download size of 536870912 bytes
sending 'rpm' (224 KB)...
OKAY [  0.039s]
writing 'rpm'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.069s
senri:~/ZS570KL $ fastboot flash tz firmware-update/tz.mbn
target reported max download size of 536870912 bytes
sending 'tz' (1600 KB)...
OKAY [  0.084s]
writing 'tz'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.114s
senri:~/ZS570KL $ 

拡張子 *.elf は 64bit ARM の ELF 実行ファイル (2個)。 *.mbn との違いは不明 (誰か教えて!)。 pmic.elf は power management IC のこと? xbl.elf は eXtended bootloader のことだろう。 それぞれ対応する同名のパーティションへ書込む:

senri:~/ZS570KL $ fastboot flash pmic firmware-update/pmic.elf
target reported max download size of 536870912 bytes
sending 'pmic' (41 KB)...
OKAY [  0.040s]
writing 'pmic'...
(bootloader) partition_need_check.
OKAY [  0.030s]
finished. total time: 0.070s
senri:~/ZS570KL $ fastboot flash xbl firmware-update/xbl.elf
target reported max download size of 536870912 bytes
sending 'xbl' (1780 KB)...
OKAY [  0.090s]
writing 'xbl'...
(bootloader) partition_need_check.
OKAY [  0.034s]
finished. total time: 0.124s
senri:~/ZS570KL $ 

mdtp.img は Mobile Device Theft Prevention のこと? ファイルの形式すら不明だが、 mdtp パーティションへ書込む:

senri:~/ZS570KL $ fastboot flash mdtp firmware-update/mdtp.img
target reported max download size of 536870912 bytes
sending 'mdtp' (8086 KB)...
OKAY [  0.324s]
writing 'mdtp'...
OKAY [  0.218s]
finished. total time: 0.542s
senri:~/ZS570KL $ 

以上で zip アーカイブの中の firmware-update ディレクトリ下の 13個のファイルの書き込みが完了した。

そして、 zip アーカイブの中の system.new.dat と system.transfer.list が、 android を起動したときに /system にマウントされる ext4 ファイルシステムのイメージ。 つまり Android OS の本体。 3.6GB ほどあり、 zip アーカイブの中の他のファイルと比べてケタ違いに大きい。

一般にファイルシステムのイメージは、 データが無い部分が散在する 「疎なファイル」(sparse file) になる。 フラッシュメモリに書込むときに、 データが無い 「0」 ばかりのブロックを書込むのは効率が悪いので、 「0」 なブロックを飛ばしたデータのファイル system.new.dat と、 そのデータをフラッシュメモリのどこへ書込むか指定するファイル system.transfer.list に分かれている。

残念なことに fastboot コマンドは system.transfer.list を扱えないので、 あらかじめ system.new.dat と system.transfer.list から ext4 ファイルシステムのイメージを作成しておいて fastboot コマンドに与える必要がある。 sdat2img コマンドを使うと、 system.new.dat と system.transfer.list から、 (疎な) イメージ system.img を作成できる:

senri:~/ZS570KL $ sdat2img system.transfer.list system.new.dat system.img
sdat2img binary - version: 1.0

Android Marshmallow 6.0 detected!

Copying 32770 blocks into position 0...
Copying 2 blocks into position 33009...
Copying 32016 blocks into position 33519...
Copying 2 blocks into position 65536...
Copying 32257 blocks into position 66046...
Copying 2 blocks into position 98304...
Copying 2 blocks into position 98545...
Copying 32016 blocks into position 99055...
Copying 2 blocks into position 131072...
Copying 32257 blocks into position 131582...
Copying 2 blocks into position 163840...
Copying 2 blocks into position 164081...
Copying 32016 blocks into position 164591...
Copying 2 blocks into position 196608...
Copying 32257 blocks into position 197118...
Copying 2 blocks into position 229376...
Copying 2 blocks into position 229617...
Copying 32016 blocks into position 230127...
Copying 2 blocks into position 262144...
Copying 32257 blocks into position 262654...
Copying 2 blocks into position 294912...
Copying 2 blocks into position 295153...
Copying 32016 blocks into position 295663...
Copying 2 blocks into position 327680...
Copying 32257 blocks into position 328190...
Copying 2 blocks into position 360448...
Copying 32257 blocks into position 360958...
Copying 2 blocks into position 393216...
Copying 32257 blocks into position 393726...
Copying 2 blocks into position 425984...
Copying 32257 blocks into position 426494...
Copying 2 blocks into position 458752...
Copying 32257 blocks into position 459262...
Copying 2 blocks into position 491520...
Copying 32257 blocks into position 492030...
Copying 2 blocks into position 524288...
Copying 32257 blocks into position 524798...
Copying 2 blocks into position 557056...
Copying 32257 blocks into position 557566...
Copying 2 blocks into position 589824...
Copying 32257 blocks into position 590334...
Copying 2 blocks into position 622592...
Copying 32257 blocks into position 623102...
Copying 2 blocks into position 655360...
Copying 32257 blocks into position 655870...
Copying 2 blocks into position 688128...
Copying 32257 blocks into position 688638...
Copying 2 blocks into position 720896...
Copying 32257 blocks into position 721406...
Copying 2 blocks into position 753664...
Copying 32257 blocks into position 754174...
Copying 2 blocks into position 786432...
Copying 32257 blocks into position 786942...
Copying 2 blocks into position 819200...
Copying 2 blocks into position 819441...
Copying 32016 blocks into position 819951...
Copying 2 blocks into position 851968...
Copying 32250 blocks into position 852478...
Copying 2 blocks into position 884736...
Copying 2 blocks into position 884977...
Copying 32016 blocks into position 885487...
Copying 2 blocks into position 917504...
Copying 2820 blocks into position 918014...
Copying 2 blocks into position 950272...
Copying 24508 blocks into position 950782...
Copying 7689 blocks into position 975291...
Skipping command zero...
Skipping command erase...
Done! Output image: ~/ZS570KL/system.img
senri:~/ZS570KL $ 

そして、作成した (疎な) system.img を system パーティションへ書込む。 fastboot コマンドは、 与えられた system.img が疎なファイルであることを認識し、 まず system パーティション全体を erase した後、 必要なブロックのみを ZS570KL へ転送して書込んでいる:

senri:~/ZS570KL $ fastboot flash system system.img
target reported max download size of 536870912 bytes
Invalid sparse file format at header magi
erasing 'system'...
OKAY [  0.002s]
sending sparse 'system' (513215 KB)...
OKAY [ 20.270s]
writing 'system'...
OKAY [  4.815s]
sending sparse 'system' (524224 KB)...
OKAY [ 20.871s]
writing 'system'...
OKAY [  5.201s]
sending sparse 'system' (523318 KB)...
OKAY [ 20.565s]
writing 'system'...
OKAY [  4.793s]
sending sparse 'system' (486869 KB)...
OKAY [ 19.371s]
writing 'system'...
OKAY [  4.688s]
sending sparse 'system' (513436 KB)...
OKAY [ 20.188s]
writing 'system'...
OKAY [  4.710s]
sending sparse 'system' (512989 KB)...
OKAY [ 20.225s]
writing 'system'...
OKAY [  4.705s]
sending sparse 'system' (506575 KB)...
OKAY [ 20.021s]
writing 'system'...
OKAY [ 22.541s]
sending sparse 'system' (67808 KB)...
OKAY [  2.684s]
writing 'system'...
OKAY [  0.475s]
finished. total time: 196.124s
senri:~/ZS570KL $ 

「header magi」 とは何ぞ? 東方より来たりし三賢者? と思ったら、 「header magic」 と表示しようとしてるのだけど、 fastboot コマンドの内部のバッファの大きさが 1 バイト足らなくて、 1 文字欠けてしまったらしい (まあ magic の語源は magi なので当たらずとも遠からじだが)。 system/core/libsparse/sparse_read.c を見ると、

static void verbose_error(bool verbose, int err, const char *fmt, ...)
{
        char *s = "";
        char *at = "";

…… (中略) ……

                size = vsnprintf(NULL, 0, fmt, argp);

…… (中略) ……

                at = malloc(size + 1);

…… (中略) ……

                vsnprintf(at, size, fmt, argp);

…… (中略) ……

                if (err == -EINVAL) {
                        sparse_print_verbose("Invalid sparse file format%s%s\n", s, at);

…… (中略) ……

struct sparse_file *sparse_file_import(int fd, bool verbose, bool crc)
{

…… (中略) ……

        if (sparse_header.magic != SPARSE_HEADER_MAGIC) {
                verbose_error(verbose, -EINVAL, "header magic");
                return NULL;
        }

などと書いてある。 せっかくメッセージの長さを調べて、 ちゃんと 1 バイト大きい (size + 1) バッファを確保しているのに、 vsnprintf の第2引数に (size + 1 ではなく) size を与えているために、 最後の 1 文字が欠けてしまうというバグ (>_<) どうしてこんな単純なバグが放置されているのか?

system/core/libsparse/sparse_format.h によると、 「sparse file format」 というファイル形式があって、 その magic (フォーマット識別子) は 0xed26ff3a であるらしい。 それなら何故 system.new.dat と system.transfer.list というファイル形式を使っているのだろう? sparse file format にすれば 1つのファイルで済むように思われるのだが...

max download size (前述の例だと 536870912 bytes) を超えるファイルを書込もうとすると、 fastboot はそのファイルが 「sparse file format」 であることを期待して、 magic が違うと 「Invalid sparse file format at header magic」 と表示する。

この場合、 通常のファイルとして読み込み直して、 内部で 「sparse file format」 へ変換するコード (sparse_read.c) になっている。 この場合、 そのファイルが疎か否かは関係なく、 単に 1ブロックが全て同じ値かどうか調べているので、 (疎でない) 通常のファイルを与えた場合も、 必要なブロックのみを転送して書込む仕様になっていた。

以上で、 Android 6.0 Marshmallow WW-4.12.40.1698 のクリーン・インストールが完了した (4月14日)。 数日間使っているが、全く何の問題も起きていない。 3月27日に問題が発覚してから 19日間、 さんざん苦しめられていたのが嘘のようだ。 もっと早く Marshmallow へ戻せばよかった。 OS のメジャーバージョンを上げるときは、 元に戻す方法を確認してからにすべきと改めて痛感した。

More...
Filed under: Android — hiroaki_sengoku @ 11:04
2016年8月3日

カナダで fido と WIND と Virgin のプリペイド SIM カードを買ってみた

8年ぶりのバンクーバー。 前回来た (2008年5月) 後にバンクーバーオリンピック (2010年) があって、 街がいろいろ変わってた。 オリンピックが終わっても不動産バブルはドンドン膨らみ続け、 巨大なビルがドンドン建てられている (が、テナントは入らず既にバブル崩壊状態?)。

でも、 それに負けず劣らず大きく変わったのがネット環境。 2008年5月当時はスマホがまだ一般的でなく (iPhone 3G の発表は 2008年6月)、 通信手段と言えばモデムだけ。 ホテルの部屋に LAN は無く、 公衆無線LAN も部屋には届かず、 かろうじていくつかのカフェで Wi-Fi が利用できたので、 わざわざカフェまで行ってネットにアクセスしていた (2008年なので Advanced W-ZERO3[es] で)。 滞在中ほとんどネットにアクセスできなかった (しかもホテルでさえネット無し!) なんて、 今から思うと考えられない。

ちなみに 2008年当時でも、 ワイキキなら多くのホテルは部屋まで LAN が来ていたし、 ホテルの部屋から公衆無線LAN でネットにアクセスすることもできた。 2008年くらいにもなるとモデムを使う羽目に陥ったことは皆無だったはず。 米国のホテルでモデムを使ったのは 2005年くらいが最後だったと思う。

8年前に泊まったホテルも、 今回のホテルも、 ロブソン通 (Robson St) に面していてすぐ近く。 8年前と違うのは周囲にケータイ屋が溢れている点。 ホテルを出てちょっと歩き回ってみるだけで、 fido, WIND, Virgin, Telus, Bell, Rogers など、 ほとんど全てのキャリアのお店を見つけることができた。 ホテルから歩いて最初に見つけた fido に入ってみる。

Fido Solutions

Fido Solutions は Rogers の子会社。 fido という名前の有名な犬がイタリアにいたらしい。 空襲の犠牲となった主人を14年間待ち続けたイタリアの忠犬フィド (イタリア語だと 「フィド」 だけど、 英語では 「ファイド」 と読むようだ)。 この犬にちなんで fido のロゴには犬小屋が描かれている。

fido のプリペイドSIM には、 月額 (By the Month) プラン ($15.75, $25.75, $30, $35, $45, $55, $60, $70) と従量 (By the Minute) プランと一日 (By the Day) プランがある。 もちろん 「$」 はカナダドル、以下同様。 月額プランのうち $15.75 〜 $35 のプランと、 一日プランは通話&SMS のためのプラン (例えば $35 の月額プランは 500分の通話が含まれていて SMS は無制限に使える) で、 データ通信に関しては従量プランと同じ。 したがってデータ通信がメインで SMS は使わない人 (= 私) にとっては、 $15.75 〜 $35 の月額プランや一日プランは (従量プランと比べて) メリットが無く、 従量プランあるいは $45 以上の月額プランが選択肢となる。

従量プラン$15.75$45$55$60$70
ローカル通話40¢/分30¢/分含300分含500分無制限無制限
長距離通話80¢/分70¢/分45¢/分45¢/分無制限無制限
夜間通話放題無し無し5pm-7am5pm-7am全日全日
国内SMS30¢/通無制限無制限無制限無制限無制限
国際SMS35¢/通無制限無制限無制限無制限無制限
データ通信要アドオン要アドオン400MB
5¢/MB
750MB
5¢/MB
1GB
2¢/MB
2GB
2¢/MB

もちろん「¢」はカナダセント、以下同様。 従量プランにはデータ通信が含まれていないので、 アドオンとして以下のデータパッケージを追加することが必須。 $45 以上の月額プランではこのアドオンを追加することはできない。

データ量月額料金超過料金
100MB$1015¢/MB
500MB$205¢/MB
1GB$305¢/MB

2週間の滞在で 1GB くらいは通信するだろうから、 選択肢としては $60 の月額プランか、 従量プランに 1GB のアドオンを追加するか、 の二択。 前者だと 1カナダドル=約80円なので 4800円くらい。 米国とかと比べると 1G で 5000円近くというのは割高感がある。 後者はそもそも 40¢〜80¢/分という通話料が高すぎ。 迷った末に $60 の月額プランを選んだ。

ところが、 支払う段階になってさらに 「Minimum Refill Amount Required $10.00」 が必要と言われる。 しかも消費税 12% が加わって、 しめて $78.40 (約 6300円)。 う〜ん、 これで 1GB しか使えないというのは高いなぁ。 「Refill Amount」 って言ってるけど、 残高 (balance) には加算されない。 預り金 (デポジット) みたいなもの? SIM 代と言ってくれたほうがスッキリするなぁ。

従量プランだと通話 1分 40¢ も課金される。 滞在中に残高を気にしながら電話するのはちょっとなぁ (残高が足らなくなってきたら refill しなきゃならない)、 と思ったので月額プランを選んでしまったのだけど、 多くても一日 5回くらいしか電話しなかったので、 2週間の滞在で合計 50分もいくかどうか。 最初に $20 くらい refill しておけば充分だった。 私の場合、 長距離通話の必要はないし、 SMS もほとんど使わない (ハングアウト等のチャットアプリを使えば済む)。

従量プランでデータ通信する場合は、 アドオンとしてデータパッケージを購入する。 1GB が $30 なので、 通話料が $20 とすれば合計で $50、 つまり従量プランのほうが $10 ほど安くなる。 滞在期間が短い場合や、 通話をあまりしない場合は、 従量プランを選択すべき。

SIM を Nexus 5X に入れてアクティベートしてもらうと、 無事 LTE が使えた。 fido は Rogers の通信設備を利用しているので LTE は Rogers と同じ Band 4 (1700/2100 MHz) と Band 7 (2600 MHz) そして現在展開中の Band 12 (700 MHz, Band 17 を内包する) が利用できる。 3G は Band II (1900 MHz) と Band V (850 MHz)。 いずれも Nexus 5X はサポートしている。

2週間の滞在で通信量を 1GB に収めようとするとかなり節約しなければならない。 Wi-Fi が使えるところでは Wi-Fi を使い、 ホテルでは Nexus 5 を Wi-Fi ルータとして使い (後述)、 できるだけデータ通信量を節約したのだけど、 ほとんど初めての土地 (8年前に来たけどほとんど忘れてた) だとどうしても Google Maps を見ることが多くなりがちで、 1日 100MB くらいは使ってしまう。 10日目くらいで通信量が 1GB を超えてしまった。

月額 $60 のプランだと、 1GB を超えたぶんは 1MB につき 2¢課金される。 仮に 1日に 100MB 使えば $2 になる。 たかが $2 と思うかもしれないが、 月額プランは残高ゼロのままで使えるのがメリットなのに、 課金が発生すると refill しなければならず面倒。

というわけで、 せっかく月額プランにしたのに、 結局 refill することになってしまった。 残高を気にして refill することになるなら、 最初から 40¢/分の通話料を払う従量プランにすればよかった。 なお、 従量プランに 1GB のデータパッケージを購入した場合は、 1GB を超えたぶんは 1MB につき 5¢課金されるので、 (1MB につき 2¢の) 月額プランより 2.5倍高く、 月額プランの意味がないとまでは言えない。

クレジットカード払いで (オンライン) refill する場合は、 カナダの住所でないとダメ。 というのも、 クレジットカード会社に登録している住所を入力する必要があるのだけど、 国名が 「Canada」 固定で変更できない (州もカナダの州から選ぶ選択式だし、 郵便番号はカナダ式でないとエラーになる)。 ところが、 クレジットカード会社に (その) 住所で認証を求めるらしい。 クレジットカード会社の判断次第というところもあるけど、 多くのクレジットカード会社は住所が異なっていると承認しない。

INTERAC Online を使う方法もあるらしいが、 Scotia Bank, RBC (Royal Bank), TD (Bank of Canada), BMO (Bank of Montreal) のいずれかに口座がないとダメ。

結局のところカナダ在住者でないかぎり refill カードを買うしかない、 ということのようだ。 これは fido に限らず他のキャリアでも同じ。 日本からの旅行者が refill カードを買うしかない、 というのは分からなくもないが、 お隣の米国から来た人でさえそうだというのだから、 いかがなものかと思う。 SIM の有効期限が切れてしまうから一刻も早く refill したいが、 それには何百キロも走ってカナダへ行ってくるしかない、 なんて笑えない話も米国にはあるようだ。

refill カードは買った日から 30日間しか有効でない (未確認) ので、 買いだめしておくわけにもいかない。 国によっては、 買ってから一年くらいは (何年も?) 有効な refill カードもあるのに、 カナダのキャリアはどうしてこう融通が効かないのだろう? ちなみに米国のキャリアも、 クレジットカード払いで refill する場合は SSN が必要だったりするので非居住者にはハードルが高いが、 幸いキャリア以外から refill カードを買うことができる (支払は PayPal が利用できる)。

More...
Filed under: SIM — hiroaki_sengoku @ 11:55
Older Posts »