仙石浩明の日記

2024年10月26日

私の FX取引 10年間の振り返り

今から 10年前、 2014年当時の私は米ドルをほとんど持っていなかったので、 ドル建て債券を買おうとすると、 まずドルを買う (円をドルに替える) 必要がありました。

ところが当時は黒田バズーカによる急激な円安が進行中だったので、 少しでもドルが安い (つまり円高) 時にドルを買いたいわけです。 一括で買えば手数料は安くて済みます (当時の証券会社は数万ドル程度の小口買いだと手数料が 1ドルあたり片道 50銭くらいしたと記憶してます) が、 小口に分けて買いコストを平均化したい (ドル・コスト平均法) ですし、 証券会社の営業時間外でもドルが安値をつけたら、 (たとえ深夜でも) その瞬間に買いたいものです。

そこで、 FX で数日間にわたってドル買いポジションを増やすことで、 ドルの平均買いコストを下げることにしました。 実際に債券を買うときに (より) 円安になっていて差損を被っても、 FX で取り戻せるわけですね。 いわば為替予約の FX 版。 2014年11月1日 01:00 に、 初めての FX 取引を行いました。 1ドル=112.108円で ドル/円 (USD/JPY) 1万通貨単位を買い建て。 つまり 1万ドルを 1,121,080円で買う予約をしたのと同じ効果があるわけです。

こうして始まった私の FX取引が、 来月 11月1日 01:00 でちょうど 10年になります。 この機会に、 これまでの 10年間を振り返ってみます。

10年前、 初めのうちは外貨の平均買いコストを下げるヘッジ取引が中心でしたが、 しだいに欲が出て、 為替変動が激しいときは積極的に利益を取りに行くようになりました。

最初はギリシャ危機再燃によるユーロ暴落でした。 2015年6月29日(月) 週明けから暴落したユーロに果敢に立ち向かったのでした。 このときの必要証拠金が 900万円超。 レバレッジ 25倍なので、 2億円を超える外貨を売買したことになります。 はじめて持った大きなポジションでした。 結果、 13時間で 200万円ほど含み益を増やしています。 ビギナーズラックだとは思いますが、悪くないですね。

2015年 当時 USD/JPY は 124円前後でしたが、 円安すぎると思ってました。 まあ、2024年の現在と比べるとかなりの円高水準ではあるのですが、 当時は米国の景気も悪くて低金利でした (米2年債が 0.7% くらい, 日本はゼロ金利)。 実際、経済ニュース等を見ていても円の実質実効為替レートが 1973年1月以来の弱さになっているという論調が多かったと記憶してます。

行きすぎた円安はどこかで揺り戻しがあるはずで、 実際 8月には USD/JPY が急落する場面もありましたし、 来年 2016年こそは円高に向かうだろうと思い、 年末から USD/JPY やクロス円を売りまくりました

ちなみにクロス円というのはドル以外の通貨 (ユーロ, ポンド, 豪ドルなど) と円との通貨ペア (つまり EUR/JPY, GBP/JPY, AUD/JPY など) のことですが、 例えば EUR/JPY は、 EUR/USD と USD/JPY を掛け合わせて (クロス) 算出することから、 クロスレートと呼ぶそうです。
つまり基軸通貨たるドルとの通貨ペア 「ドルストレート」 (EUR/USD, GBP/USD, USD/JPY, AUD/USD, USD/CAD など) だけ決めておいて、 それ以外の通貨ペアのレートは掛け算で算出するわけです。 で、円を含むクロスレートだから 「クロス円」。
確かに実需ではドルとの取引が大半 (9割近く) ですが、 為替取引全体における実需の占める割合は 1割未満に過ぎないので、 クロス円という呼び方が適切なのか疑問だったりします。
(日本の) FX では円との取引のほうが圧倒的ですからねぇ。 むしろ円ストレートとか呼んだ方が実体に即しているかも知れません。 スプレッドも円との通貨ペアのほうが、 ドルストレートより狭くなりがちですし。

ところが 2016年1月29日(金) に日銀がマイナス金利導入を決定、 それを受けて USD/JPY が 2円も急騰した (100万通貨単位くらい円を買い持ちしていたので含み損が一日で 200万円も拡大した) ので怖くなり、 ポジションを縮小してしまいました2月から再び怒涛の円高 (2月11日には 110円台、6月には 100円割れ!) が進んだわけで、 円買いポジションを維持していれば爆益でした。 千載一遇の大チャンスを逃してしまったわけで、 なかなかうまくいかないものですね。

同じマイナス金利ネタでも、 2016年4月28日の 「日高ショック」 による 3円の円安には耐えることができました。 「日銀:金融機関への貸し出しにもマイナス金利を検討-関係者」 というデマが、 Bloomberg で流れたのです。 市況かぶ全力2階建でも紹介されました。

世界的な景気悪化と原油価格の下落でリスク回避 (当時は 「リスク回避 = 円高」) が強まる中、 2016年6月24日のブレグジットが混沌に拍車をかけました。 怒涛の円高で 98円台へ突入したのでした。 ポジションを縮小してしまったとは言え、 ドラギ・マジックなどもあって、 8月までの円高で 500万円ほどの利益を得ました

2016-02-01 USD-JPY 1-day chart

FX を始めて 3年くらい経ったとき疑問に感じたのは、 「なぜ FX で損をする人が多いのか?」 です。 FX会社が儲かるのは損する人が多いから (店頭FX) ですよね。 大きく勝つのは難しいにしても、 年率 10% くらいなら無理なく稼げると思うのですけれど。

中の人に知り合いはいないので全くの想像ですが、 店頭FX って顧客を何段階かにランク分けして、 「カモ」はカバー率を下げてるんじゃないかなぁ? 全ての注文 (の合計) をバカ正直にカバー先へ取り次いでるだけだと大して儲からない (注文が一方向に傾くと、昨今のスプレッドは狭すぎて逆鞘になってしまいます。 まあ、大抵は売り買い同数に近くて、 狭くてもスプレッドぶん丸儲けなのかもしれませんが…) ですよね? カモの注文を取り次がなければ、 カモが損した分がそのまま儲けになりますから。 ランクの最下位は「逆神」かな? 売り買い逆にしてカバー。
2018年に始めた取引でいきなり 100万円以上の損失を出した私はカモ認定されたハズで、 2022年に取引を再開したとき、 私の注文はカバーしなかったんじゃないかと妄想してます。 最大 400万円の損害を与えたかもしれませんね。

結局のところ、 みなさん早すぎる損切り (あるいは早すぎる利益確定) で儲け損なっているのでしょう。 前述の例で言えば、 円高になると確信していても、 マイナス金利で市場が動揺して (一時的に) 円安に振れると、 怖くなってポジションを縮小してしまう。 儲け損なうのは、いつもそんなパターンです。

もちろん、 不本意な損切りをしなくて済むような資金的余裕、 あるいは同じことですが身の丈に合った投資額に抑えることは必須ですね。 強制ロスカットまで行かなくても、 資金の底が見えると、 どーしても早めに損切りしたくなってしまいますから。

ある日突然レバレッジが 1倍になっても (後述するように、 ウクライナ戦争のときにルーブルが突然 1倍になって、 夕方から 8時間で 800万円ほどかき集める必要がありました)、 全く動じずに証拠金を積み増すことが可能な範囲で取引したいものです。

新規注文を出す際、 逆指値注文を同時に出しておくと、 為替が想定と逆方向に動いたとき自動的に損切りできるので便利ですが、 為替が大きく動く時って、 得てしてまず逆方向に動いたりするものです (というかそれを意図的に仕掛ける人たちがいますね)。 逆指値注文が早すぎる損切りにつながっていないか確認することが重要でしょう。

さらに言えば逆指値注文の執行方法にも注意が必要です。 FX会社の多くは、 逆指値の売り注文はビッド (FX会社が買ってくれる価格) を参照しますが、 スプレッド (ビッドとアスクの差) が開くと逆指値が刈られてしまうんです。 早すぎる損切りが多発する一つの要因でしょう。
逆指値の売り注文はアスク (FX会社が売ってくれる価格) を参照してくれれば、 このような問題は起きませんが、 そういう FX会社は残念ながら少数派のようです。

以上のような考えで、 逆指値をつけずに取引することが増えていきます。 FX を始めてから 2017年までの 3年間、 毎年 100万円以上、 累計だと 700万円の利益を得た (ビギナーズラック?) ので、 少しばかり気が大きくなったというのもあるのでしょう。 各年の FX会社ごとの損益 (各年の確定申告書から転記しました) はこんな感じ:

SBI FXαライブ
スター
DMM.comIGサクソ
バンク
ヒロセ通商楽天
2014890,6501,059,0771,949,727
20151,570,747-346,400-81,66617,2031,159,884
2016298,8061,600,651-603,791131,7541,427,420
2017759,1001,699,653-4,4202,454,333
2018-1,341,250895,513-1,131,775-1,577,512
2019-52,4474,867,7674,815,320
2020-307,090-9,687,265-9,994,355
20211,900,38313,999,03715,899,420
2022841,193-7,485,726996,500-5,648,033
2023-1,331,7123,043,8552,942,3674,654,510
20242,099,06510,568,14512,667,210
890,6502,928,6303,054,1404,370,875144,5372,908,58013,510,51227,807,924

どの FX会社の取引アプリが自分に合っているか? いろいろ試してみる過程でどんどん FX会社を変えています。 2018年に新たに試したのはヒロセ通商でした。 始めての FX専門業者 (有価証券関連業を行なわない会社は「証券」の商号を付けられない)。 「レートが上昇するとともに逆指値注文の値も上昇していく」 トレール注文を試してみたかったのです。

ところがこのトレール注文が、 わたし的には最悪でした。 逆指値がレートについていくものだから、 上昇基調にある通貨がちょっと下がるだけですぐ逆指値にヒットしてしまい、 大きな利益を取り損なってしまうのです。 コツコツ損してドカンと利益を取りに行く私の投資スタイルには全く合いません。

結局、 わずか 3ヶ月でヒロセ通商は 100万円を超える損失を出し、 2018年全体としても初めてのマイナスに沈みます。 もうヒロセ通商は使うものかと、 いったん資金を引き揚げました。

が、負けたまま退散したというのはどうにも心残りで、 取引アプリの使いにくさが改善されたのを機に 2022年11月にヒロセ通商の取引を再開し、 翌 2023年と合わせて 400万円の利益を出し、 累計で 300万円近いプラスを達成、 ぶじ勝ち逃げすることができました。:-)
ヒロセ通商に限らず、 取引したすべての FX会社で、 最終的な FX損益がプラスなのが自慢です。
なお、 楽天証券の利益が突出していますが、 これは楽天証券が私に向いているからではなくて、 楽天証券の FX を使い始めた 2023年以降は、 IG証券を主に 「プランB」 用に使っているからです。 想定通りにコトが進めば、 利益の大半は 「プランA」 用の楽天証券に偏ります。

トレール注文を使ってみて、 「早すぎる損切りをしていては勝てない」 という考えは確信に変わりました。 また、 7年以上も使い続けた DMM.com を 2022年に止めたのは、 取引アプリで損切り幅が 99pips までしか設定できないからです。 損切り幅が (最大でも) わずか 99pips というのは、 早すぎる損切りばかりになり、 わざわざ負けるために FX をやってるようなものだと思うのですが…

2018年はマイナスに沈み、 さらに 2020年のコロナ禍と 2022年のロシアの侵略という 2つの厄災で大負けしたにもかかわらず、 10年を通してみると平均して年 278万円くらい稼げているわけで、 (意外と) 堅実な運用ができていると思います。

コロナ禍や戦争、あるいは為替介入などの有事を除けば、 ふだんの必要証拠金は 600万円を下回ることがほとんどです。 必要証拠金を投資額と考えれば、 年率 46% の運用になるわけで、悪くないですよね? :-) さらに 2019年以降に限定すれば (コロナ禍や戦争にもかかわらず) 年率 62% にも達します。

2018年までは、 ドカンと儲けている一方で初心者にありがちなミスで損失も大きかったようです。 例えば、 この表では 2016年の利益が 140万円になっていますが、 前述したように 2016年は 8月までに DMM.com で 500万円以上儲けたハズ…?

不思議に思って過去の取引履歴を掘り起こして見てみると、 2016年10月3日(月) からの わずか 1週間で 400万円以上を失っていました (;_;)。 月曜日は 1.29ドル台だった ポンド/ドル (GBP/USD) が、 金曜日には 1.18ドルまで実に 8.5% も下落しています。 特に金曜日 (2016年10月7日) は 1日で 6.1% も下落、31年来の安値を更新しました。

まさに 「落ちてくるナイフは掴むな」 の典型なんですが、 もう少し細かく見ると、 金曜日 21:30 に 1.23866ドルで買って 2分後に 1.23576ドルで売って 30,035円 損した直後、 21:33 に 1.23310ドルで売って 24分後に 1.23814ドルで買って 26,037円 損しています。 典型的な 「往復ビンタ」 ですね。まさにありがち… (>_<)

相場の乱高下にうまく乗れないときは、 いったん休むのが重要ですね。 うまく乗れると乱高下は大チャンスだったりするのですけれど。 例えば、高市〜石破ショックは、うまく波に乗れて 4時間で 70万円ほど稼ぎました。

2018年に初めての年間マイナスを経験して、 気を引き締めてのぞんだ翌 2019年は一転 500万円近いプラスでした。 勝因は、 言わずと知れたトルコリラのスワップポイントです。

ドル/リラ (USD/TRY) 22万通貨単位を売り持ち (つまり リラ買い) していたのですが、 2019年3月28日のスワップポイントが 376,002円にもなりました (1万通貨単位あたり 17,091円)。 想定元本は 22万ドル (24,419,859円) なので、 年率 548%! 闇金 (トイチ) もビックリ。 :-)

2019年3月28日のスワップポイントは異常値としても、 3月27日〜4月2日の 7日間の合計で 717,288円もあったので、 平均すると 1万通貨単位あたり 4,658円/日、 年率 153% ということになります。 つまり 1年で 2.5倍。
スワップポイントは FX会社によって異なりますが、 このときのトルコリラは、 IG証券のスワップポイントが特に (異常に?) 高かったようです。
ちなみに必要証拠金は 92,004リラ (1,822,240円) でした。 レバレッジは 25倍じゃなくて 13.4倍に下がっていたようです。 トルコリラは変動が激しいからでしょうね。

そして翌 2020年はコロナ禍ショックです。 新型コロナウイルスの世界的な感染拡大や原油相場の急落がリスク回避を加速させ、 豪ドル/ドル (AUD/USD) は 0.70ドル (2020年初) から 0.55ドル (2020年3月19日) へ実に 21% も下落しました。

2020-03-19 AUD-USD 1-day chart

市場がリスク回避的だと豪ドルは売られやすく、ドルは買われやすいので、 AUD/USD の下落が特に大きくなりますが、 他の通貨ペアもあり得ないようなレートになり、 逆指値をつけていなかった私も損切りせざるを得ず、 1000万円近い損失をかかえました

ただ、損切りで損失を確定させる一方で、 底値では果敢に買いを入れました。 FX同様、ありえない価格 (史上初のマイナス価格!) になった原油先物についても (よく分からないまま) 原油ETF を買ってみました

結果的にこの底値買いが奏功し、 1年後の 2021年2月下旬には、AUD/USD が 0.8ドルに達するなど各通貨ペアで軒並み含み益が拡大し、 原油ETF と合わせて 1800万円の利益を得ました。 結果的に 2020年の損失の倍返しとなっています。:-)

そして翌 2022年はロシアのウクライナ侵略です。 2018年以来、 強いドルの影響で割安に放置されていた新興国通貨を買っていたのですが、 その中にロシアルーブル (RUB) もありました。

2022年2月24日(木) にロシアが侵略を開始した時に暴落したルーブルですが、 いずれ元の水準に戻るはずと考えてポジションを維持したままでした。 当時の USD/RUB のレートは 1ドル=82.73ルーブルでしたが、 さらにルーブルが下落する可能性を考えて 2月27日(日) に証拠金を積み増していたのです。

ところが、2022年3月1日(火) 16:04 に IG証券から必要証拠金率の引き上げの通知が来ました。 それまで 20倍だったルーブルのレバレッジが、 いきなり 3月2日から 1倍になったのです。 2月27日(日) に証拠金を 700万円 積み増していたのに、 さらに 3月1日(火) に 800万円 積み増す羽目に陥りました。

計 1500万円も短期間で集めるのは大変です。 しかも猶予は 3月1日 16:04 から 8時間しかありません。 すでに金融機関の窓口が閉っているような時間にどうしろと? このときは複数の銀行・ 証券会社の口座に分散して置いていた予備の資金をかき集めました。 有価証券の売却は時間がかかるわけで、 いざという時に備えてキャッシュを残しておくのは重要ですね。

ところが、それで終わりではなかったのです。 翌 3月2日 14:36 に強制決済を予告するメールが来ました。 2022年3月4日(金)午前0時 (つまり 3月3日の晩) 時点で、 ルーブルの未決済ポジションの強制決済を行うとのこと。 ご丁寧なことにメールに続いて 15:33 に電話もかかってきました。 こんな事態になってもポジションを維持し続けていた人って、 すごく少なかったのでしょうね。

もう猶予は 32時間余りしかありません。 翌 3月3日に USD/RUB が暴騰したら目もあてられません。 仕方ないので 3月2日 16:01 ごろ USB/RUB の取引が再開された直後に 1ドル=109.5ルーブルで 13万ドル分を手仕舞い、 トータル 471万円の損失を食らいました。 強制決済さえなければ、 3月30日には侵略前の水準 1ドル=84ルーブル台に戻ったので、 損失どころか利益が出ていたのですが。

ウクライナ戦争のインパクトが強すぎて注意が疎かになっていたのですが、 2022年は FRB が急激に利上げした年でもありました。 コロナ禍対策のため直前までゼロ金利だった (利上げ開始が遅すぎた) ことが急激さに拍車をかけました。 米2年債のチャートを見ると、 その利上げの速さ、到達点ともに、 いかに 2022年の利上げが異次元だったか分かります。 異次元過ぎて金融システム不安を引き起こしました

2014-2024 US02Y 1-month chart

2022年2月下旬に USD/JPY は 114円台だったのですが、 3月7日(月) から急に上昇を始めます。 どんどん進むドル高を目の当たりにしてあろうことか私は、 こんな流れがいつまでも続くハズはないと 123円超えあたりから徐々にドル売り (ドル以外の通貨買い) に転じてしまったのです。

当時の私は 2015年6月の 125円あたりをドルの高値のメドと安易に考えていたのでしょう。 当時はまだ 「有事の円買い」 と言われていましたし、 これから円が独歩安になるとは夢にも思っていませんでした。

USD/JPY は 3月に 125円に達した後、 いったん下がりますが 4月からは再び上昇に転じて 125円を軽々と超え、 6月には 135円、 9月には 145円を超えてしまい、 損失がどんどん膨らみます。

今から振り返れば、 もともと戦争による 「有事のドル買い」 でドル需要が高まっていたところに、 日米金利差の急拡大でキャリートレード (円売りドル買い) の需要が一気に増大したのだろうと思うわけですが、 2022年の渦中にあっては 「行きすぎた円安」 と見えてしまうのも仕方がない気もします。
6月の時点で既に 20円も円安になってるのに、 これから年内のうちにさらに 20円も円安になるとは、 ふつう予想しませんよねぇ?
バブルがどこまで膨らむかなんてことは予測不可能なわけで、 むしろ損失が膨らむのは不可避と諦め、 バブルが崩壊する過程 (2022年11月11日の CPI ショック) で損失を取り戻すほうがマシのような気もします。

損切りが遅いわけでは決してなく、 途中何度も損切りしているのですが、 逆張りを試みては損切して何百万円か失う、 を何度も繰り返しているうちに損失がどんどん累積していきました。

2022年9月22日(木) の介入で多少は挽回できたものの、 これでドルはようやく下落に転じるだろうと予想してしまい、 さらに傷口を拡げる結果となりました。 10月には USD/JPY が 152円に達してしまい、 年初からの損失の累計が 1600万円 (ルーブル強制決済の損失を含む) に達してしまったのです。

2022年9月22日(木) の介入は、 すぐ全モ (全戻し) になって 「神田暴威」呼ばわりされたのですが、 10月21日(金) と24日(月) にも介入を実施したことで USD/JPY の上昇がようやく止まりました。 この 3回の介入額は合計 9兆1880億円だったそうです。

円ほどではなかったにせよ、 (ウクライナ戦争の影響をモロに受ける) ユーロも異常事態で、 2022年7月12日には (ギリシャ危機でも割れなかった) パリティがついに割れてしまったのでした。 ドルインデックスは 2022年9月28日に 20年ぶりの高値 114.78 を記録しています。 当時は円だけでなくドル以外は全て弱かったわけで、 円だけが弱い 2024年の円安 (ドルインデックスは 2024年4月16日の 106.58 がピーク) とは違いますね。

年末が近づいてきた 11月初め、 このままでは記録的な年間損失になるなぁ… と半ば諦めていたところ、 2022年11月11日の CPI ショックで 800万円以上挽回し、 さらに 12月19日の日銀の利上げで 200万円以上取り戻すことができました。 結果、 1年のトータルでは 565万円という常識的な額の損失に抑えることができました。

twitter などを見ていると、 この CPIショックで大損した人もいるようですが、 CPI 自体は多少予想を下回った程度で、 米景気はまだまだ底堅いことを示しています。 にもかかわらず 「CPIショック」 が起きたのは、 それだけ 米ドル バブルが膨らんでいて、 CPI 発表は最後の針の一突きだったということでしょう。
つまりこの CPI 発表前の局面ではドル売りが正しくて、 ドル買いはキケンだったということです。 私は必要証拠金が 1000万円を超える (つまり想定元本が 2.5億円超) ほどドル売りポジションを膨らませていたので、 この CPI ショックで 800万円もの含み益を得ることができました。

翌 2023年は、 2期10年を務めた黒田日銀総裁が退任し、 2023年4月に植田総裁が就任しました。 就任早々利上げすることは無いだろうし、 米国の景気もしばらくは安定しているだろうとふんで、 5月からスワップポイント狙いに宗旨替えしました

少しでも波乱があると、 スワップポイントによるコツコツ利益なんて一発でドカンと吹っ飛ぶわけですが、 幸いなことに 5月から 11月まで半年間も続けることができて、 スワップポイントだけで 300万円を稼ぎました

既に口座を持ってる (FX 取引が可能な) 証券会社の中では、 楽天証券が一番スワップポイントが高かったので、 このときから楽天証券に乗り換えています。 2023年11月21日時点での GBP/JPY のスワップポイントが 1万通貨単位あたり 295円、 必要証拠金が 74,240円だったので日歩 40銭。 USD/JPY だと 220円と 59,370円で日歩 37銭。

このとき楽天証券における全ポジションの必要証拠金合計が 5,421,580円で、 スワップポイントの合計が毎日 20,045円でした。 もしこの状態を 1年間続けることができたら、 542万円の必要証拠金で 732万円を稼ぐことができることになります。 元手が 2.45倍になるのだから魅力的ですね。 為替が安定していれば、ですが。

2023年10月19日に米2年債の金利が 5.26% のピークをつけたあと下落に転じたので、 11月に入ってからは USD/JPY を一部手仕舞って GBP/JPY や EUR/JPY の比重を高めていました。

ところが 2023年12月7日(木) に、 植田総裁の 「年末から年始にかけてチャレンジングな状況になる」 発言、 いわゆる 「植田チャレンジング・ショック」 で 147円台だった USD/JPY が一気に 141円台へ突入してしまいます。 もちろん GBP/JPY と EUR/JPY も揃って下落、 600万円もあった含み益が 200万円まで減ってしまいました。 こういうことが起きるからスワップポイント狙いは怖いですねぇ。 先が見えなくなったので、 いったん手仕舞いました。

12月19日(火) に植田総裁は 「チャレンジング」 の意図を問われ、 「一段と気を引き締めてというつもりで発言した」 と弁明しています。 まったく人騒がせな話です。 とはいえこれで状況は 11月と何も変わっていないということになりましたから、 気を取り直して GBP/JPY を中心に再びスワップポイント狙いです。

2022年に続き 2023年も円安基調だったので、 スワップポイント狙いには最適な年でした。 2022年11月の CPIショックをきっかけに急激に進んだ円高でしたが、 2023年1月16日(月) の 127.22円が USD/JPY の底だったように見えます。
2022年の始めと比べると 15円も円安ですが、 たとえ日米金利差が縮まっても、 もう 120円を割るような水準には戻らないかもしれません。 唐鎌氏が 「弱い円の正体 仮面の黒字国・日本」 で述べているように 「強い円」 の時代は終わってしまったのだと思います。
おそらく、 コロナ禍で急激な円高が進んで USD/JPY が底値をつけた 2020年3月9日(月) の 101.18円が、 最後の 「有事の円買い」 となるのでしょう。 コロナ禍がまさに歴史的な転換点だったのだと思います。 2021年1月6日(水) の底値 102.59円以降は、 一貫して円安傾向が続いています。
ちなみにその前の底値は 2018年3月26日(月)、2019年1月3日(木)、2019年8月26日(月) で、 いずれも 104円台半ばです。 米中貿易摩擦の激化や中東情勢の悪化などで世界経済が減速した時期ですね。 当時はまだリスクオフといえば円高でした。 ちなみに 3つの底値のうち真ん中 2019年1月3日(木) はフラッシュ・クラッシュで、 その前後は 110円以上で推移していました。

翌 2024年の 2月からメキシコペソ (MXN/JPY) にも手を出しています。 新興国通貨はいつ暴落するか分からないので少額ですが、 10万通貨あたり MXN/JPY のスワップポイントが 260円、 必要証拠金が 35,300円なので、 日歩 73銭です。USD/JPY の倍ですね。 もし 4半期に一度スワップポイント (元手の 67.2%) を再投資しつつ 1年間続けることができたら元手が 7.8倍! (=1.672^4) にもなるわけです。 まあその前に為替差損でやられるでしょうけど。

当時最強通貨だったペソですが、 2024年4月9日に USD/MXN が 1ドル=16.26ペソ (MXN/JPY 9.3円) の 2015年以来の最安値 (ペソ最高値) をつけたあと上昇 (ペソ下落) に転じ、 4月19日に 1ドル=18ペソ (MXN/JPY 8.6円) まで大暴騰 (ペソ大暴落)し、 いったん戻すものの再び上昇 (ペソ下落) し 1ドル=19ペソ台 (MXN/JPY 7円台) で推移しています。

もしこのときまでスワップポイント狙いを続けていたら、 それまでの利益を全て吹っ飛ばして、 それよりはるかに大きい含み損を抱えるところでした。 スワップポイントを狙うのは為替が安定しているときに限りますね。 というか新興国通貨は暴落しがち (典型はトルコリラですね) なので、 スワップポイントを狙うのは時期を選ぶべきでしょう。 ちなみにコロナ禍のときはペソが大暴落して、 2020年4月6日に USD/MXN が 1ドル=25.78ペソ (MXN/JPY 4.2円) の最高値 (ペソ最安値) を記録しました。

4月19日のペソの大暴落に買い向かった後、 (急な円高に備えるべく) ポジションを徐々に円買い (豪ドル売り) へシフトしていました。 止まらない円安に打つ手は無いと思いましたが、 それでも日銀が何かするんじゃないかと警戒していたのです。 そして今年のゴールデンウィーク直前、 2024年4月26日(金) の日銀総裁会見で市場に衝撃が走ります。 植田総裁の「円安は物価の基調に大きな影響なし」発言ですね。

わたしは滅多にドテンをやらないんですが、 植田総裁の発言を知って、 この時ばかりは全力でドテン円売りを決行しました。 遅いランチを食べてくつろいでいた時だったのですが、 スマホで夢中で円を売りまくって (USD/JPY, EUR/JPY, AUD/JPY, MXN/JPY, NZD/JPY, CAD/JPY 買い) 気づいてみれば必要証拠金が 1500万円超。 4億円近く円を売ったことになります。

そして週が明けてゴールデンウィーク真っ最中の 2024年4月29日(月) こんどは円買い介入です。 散歩中に 13:05 から始まった USD/JPY の急落 (159.5円→158.2円) を見て、 13:06 から円を買いまくりました (USD/JPY, EUR/JPY, GBP/JPY, AUD/JPY, MXN/JPY, NZD/JPY, CAD/JPY 売り)。

5月2日(木) 05:10 にも再び介入が行われて、 歴史に残るゴールデンウィークになりました。 介入額は 4月29日が 5.9兆円、5月1日が 3.8兆円だったそうです。 神田暴威の面目躍如ですね。

USD-JPY 1-hour chart

そして最後が植田ショック、 いわゆる令和のブラックマンデーですね。 2024年7月31日(水) の金融政策決定会合後の記者会見で、 植田総裁がさらなる利上げを否定しなかったことで急激に円高が進み、 週明け 8月5日には先週比 10円ほど円高の 144円台に突入し、 株価が大暴落しました。

植田総裁の発言をきっかけとしたショックって、
2023年12月7日(木) の 「年末から年始にかけてチャレンジングな状況になる」
2024年4月26日(金) の 「円安は物価の基調に大きな影響なし」
に続いて 3度目ですよね? ちょっと多すぎません?

その 3週間前、 2024年7月11日(木) 21:30 の米 CPI 発表で、 USD/JPY が 161.5円から 158.3円へ 3.2円ほど急落しました。 一方、日本でも 7月11日 or 12日に 5.5兆円の介入が行われたと言われています。 確かに 7月12日(金) 8:25, 8:45, 22:05 に 1円ほどの急落が起きていますが、 介入にしては下落幅が小さいので、 もしかすると CPI 発表のタイミング 11日(木) 21:30 に合わせた (まさにステルス) のかもしれません。

「植田ショック」 というよりは、 CPI & 介入 & 植田発言の 3連発が、 植田ショックを増幅した (3倍増?) のでしょう。 私は 3連発最初の CPI 発表のタイミングで円買いへシフトしていたので、 3連発最後の植田ショック後 8月6日(火) 01:09 (未明) に手仕舞って 200万円ほどの利益を得ました。 これでしばらくは利上げが不可能になったと思い、 8月6日(火) 08:00 (つまり令和のブラックマンデーの翌朝) からは円売りです。 さらに 8月7日(水)、 日銀副総裁が 金融資本市場が不安定な状況で「利上げをすることはない」と明言したので、 安心して円売りを拡大しています。

9月に入って円高へ向かったので損切りして一時的に円買いへシフトしましたが、 2024年9月27日(金) の自民党総裁選で石破氏が選ばれて一気に円高になったのを受けて円売りに戻しています。

細かく言うと、 高市氏優勢で円安が加速した 9月27日(金) 12:52 に USD/JPY を損切り (高値で買い戻し) し、 高市氏が勝ったら USD/JPY を買おう (つまり円売り) と待ち構えていたところ、 14:38 あたりで石破氏が勝ちそうな感じがしてきた (ふだん録画でしか見ないテレビをずっとリアルタイムで見てました。 開票作業を どアップしてくれたので非常に参考になりました) ので一気に USD/JPY を売り (つまり円買い)、 実際に石破氏が勝ってから USD/JPY を一部買い戻しています。

本当は麻生氏が高市氏支持を決めた時点でドテン円売りすべきだったと反省。 そうすれば高市氏が優勢になるにつれ円安が進んだ局面でも稼げたのに、 ドテンどころか高市氏の優勢が報じられて円安の流れが確定的になるまで、 損切りすらためらってしまいました。 私は 27日(金) 午前中の時点では、 高市氏の優勢がどの程度なのか分かっていなかったのです。 今回のように誰が選出されるかで円高/円安がはっきり分かれるときは、 もっと注意深く動向を見守るべきですね。

石破氏が自民党総裁に選出されて円高が一服した 9月30日(月) 朝から、 もちろん円売り (USD/JPY, EUR/JPY, GBP/JPY, AUD/JPY, CAD/JPY 買い) です。 石破氏が金融緩和基調を変えないことを明確にするにつれ、 円安の流れが決定的になりました。

その後は現在に至るまで円売りポジションを継続していますが、 明日 10月27日(日) の衆議院選挙 (公示直後に期日前投票を済ませました) で与党が過半数割れしても、 先行き不透明感が増して円高へ向かうというよりは、 日銀の利上げがいっそう不可能になって、 しかも景気の先行きも不透明になって、 与党が過半数を維持できた場合よりも円安へ向かいそうなので、 このまま円売りポジションを維持すべきだろうと考えています。

映画 「シン・ゴジラ」 が 2016年に公開されたとき、 「1ドル=167円! ゴジラ上陸なら円安襲来!?」 という記事がありましたが、 いまや 167円も時間の問題であるように思えます。 この 8年は 「静かなゴジラ襲来」 と言っても過言ではないのでしょう。 まあ、ホントにゴジラが首都圏に襲来したら、 そんなレベルじゃ済まないと思いますが… (^^; 日本経済の 「壊滅」 じゃなくて 「消滅」 ですね。 円の価値は限りなくゼロに近づくでしょう。

こうして振り返ってみると、 この 10年は、 強い円から弱い円へ大きく変化した転換点だったように思えます。 前半は有事の円買いだったのに、 後半は有事だと円が売られるようになりました。 この 10年は黒田日銀総裁の 10年にほぼ重なるわけですが、 黒田バズーカ、 すなわち 「異次元の金融緩和」 がこの大変化を引き起こしたのでしょうか?

資金は最も有利なところへ流れます。 1980年代の金融緩和 (公定歩合を 9% から 2.5% へ引き下げ) のときは、 「Japan as Number One」 だったので、 供給された資金は国内に留まり円高とバブル景気を引き起こしました。

2013年から始まった異次元の金融緩和では、 供給された資金が国内企業への貸付へ向かうハズはなく、 国内よりずっと有利な投資先である海外へ流れてしまいました。 不動産関連へ流れて地価高騰を招いたバブル期のほうが、 国内に留まっていたぶんまだマシだったかもしれません。 世界中で余った資金は主に米国へ流れてドル高とインフレを引き起こし、 FRB の急激な利上げにつながっています。 こう考えると、 黒田バズーカがそもそもの始まりだった可能性はあると思います。 そこにコロナ禍とウクライナ戦争が重なったのは単なる偶然なのでしょうか。

来年 2025年はプラザ合意から 40年です。 日本の国運における40年周期説というのがありますが、 なにか諸々のことが関連しているように思えてなりません。 たくさんのことがこの 10年のうちに起きました。 仕事をしていたらそのほとんどを見過ごしていたかも知れません。 そもそも介入があっても、 仕事中だと対応できませんからねぇ。 いつ、どのようにして 「敗戦後の復興」 は始まるのか? これからも世界の動きを注意深く見ていく必要性を痛感しています。 13年前に FIRE していて本当によかったと思います。

Filed under: 経済・投資・納税 — hiroaki_sengoku @ 07:57
2024年3月21日

突然死した Pixel4 から nanaco 残高を救い出した話

何の前触れもなく Pixel 4 が壊れた。 寝る前フツーに使っていたのに、 朝 7:00 ごろ見ると電源オフ状態になっている。 電源ボタンを長押しても何の反応もない。

単なるバッテリー切れ? と思って USB ケーブルをつないで充電してみる。 が、 画面は暗いまま。 電流を測ってみると 200mA ほど。 充電が行われているようには見えない。 もちろん USB の信号線にも何の応答もない。 「電源ボタン + ボリューム小」を長押し (強制再起動) しても無反応。

自宅の Wi-Fi ルータのログを見ると、 朝 6:46:06 に Pixel 4 から Google (*.1e100.net) に対して https アクセスをしたのが Pixel 4 からの最後の通信。 朝方までは正常に動いていたみたい。 自宅の Wi-Fi に接続しているスマホは、 その状態を監視するために、 定期的にサーバから ping を打っているが、 6:51:21 に打った ping には応答がない。 つまり起床直前、 06:46 〜 06:51 ごろ Pixel 4 が機能を停止したのだろう。

一週間ほど USB ケーブルをつないだまま放置したが、 ついに何も変化は起きなかった。 バッテリーや充電回路の問題というよりは、 ロジックボードの突然死が疑われる。 つまり分解してバッテリーを直接充電したとしても、 復活する望みは薄い。

機種変更できるアプリは、 速攻で他のスマホにインストールし直したが、 問題は おサイフケータイ。 Suica や nanaco には残高があったはず。

Suica は PC 等でモバイルSuica会員メニューサイトにログインして、 利用停止手続き を行うだけ。 10分後に新しいスマホに引き継げる。 翌朝 5:00 までは残高は 0円と表示されるが、 それ以降に確認したら残高が復活していた。

ところがセブン・カードサービス (nanaco の運営会社) は、 会員メニューサイトにログインするだけでは本人確認が充分でないと考えているらしい。 他人が (何らかの方法で窃取したパスワードを使って) 勝手にログインして利用停止手続きを行い、 残高を奪うことを恐れているのだろう。 まあ、nanaco のパスワードなんてテキトーに設定している人も多いだろうから、 その懸念は理解できなくもない。

nanaco の場合、 まず nanaco の機能停止を行った上で 「引継番号」 を発行し、 その「引継番号」を使って新しいスマホに nanacoモバイルアプリをインストールする。 ただし、 この段階では nanaco 残高は引き継がれない。 セブン・カードサービスから送られてくる 「nanaco引継申請書」に新しいスマホの nanaco番号を記入し、 本人確認書類 (マイナンバーカードなど) のコピーを添付して返送する必要がある。

この引継番号は、 機種変更の際の「引継番号」と同じものと思われるが、 機種変更の場合は残高が直ちに引継がれる点が異なる。

「引継番号」の発行までは以下のように WEB で手続きできる:

nanaco transfer 1

Suica のように会員メニューから故障 (or 紛失) したスマホの Suica の機能停止ができれば簡単なのだが、 nanaco の場合は、 氏名・生年月日・電話番号 (以下、「登録名義」と略記) を指定することで、 故障 (or 紛失) したスマホの nanaco (以下、「紛失nanaco」と略記) を特定するらしい。

つまり登録名義さえ分かれば、 他人が勝手に nanaco を機能停止できる? これって会員メニューサイトのパスワードを窃取するより簡単じゃない? 誕生日なんて facebook 等で公開している人が多いような… (ちなみに私は公開していない)

まあ、 紛失nanaco のユーザ全員が会員メニューにログインできるとは限らない (そもそもパスワードを忘れてしまっていたりする) から、 このような方式にしているのだろう。

More...
Filed under: Android — hiroaki_sengoku @ 08:31
2023年7月5日

exFAT な Ubuntu ブータブル USBメモリ (exFAT Bootable USB Flash Drive) の作り方

USBメモリや DVD などのリムーバブルメディアから起動可能な Ubuntu (以下 「Live Ubuntu」 と呼ぶ) は、 PC がトラブったときのレスキュー (障害復旧) の道具として重宝する。 最近の USBメモリは容量が大きく、 3GB 程度の Live Ubuntu を入れておいても大して邪魔にならない。 ふだん持ち歩く USBメモリにも、 それぞれ Live Ubuntu を入れておくとイザというとき便利。

ところが、 Ubuntu (公式) が公開している Live Ubuntu は exFAT からの起動に対応していない。 exFAT は FAT の後継として Microsoft が開発したファイルシステムで、 従来 4GB までのファイルしか扱うことができなかった FAT の制約が大幅に緩和されている。

昨今の動画ファイルはサイズが 4GB を超えるものも多く、 USBメモリは FAT ではなく exFAT でフォーマットしたい。 もちろん、 NTFS でフォーマットすれば大きなファイルを入れられるし、 Live Ubuntu も起動できるが、 NTFS は USBメモリには牛刀すぎる。

USBメモリは様々な機器に挿す可能性があるわけで、 NTFS にするのは躊躇してしまう。 スマホやラズパイ、 さらにはコンビニ等のプリント機など、 その全てで NTFS が問題無く使えるのだろうか? やっぱりデフォルトである exFAT のほうが安心。

Ubuntu などのインストールメディアの ISO イメージを exFAT に置いて起動する方法 (Make an exFAT Bootable USB Flash Drive) が既に公開されているが、 仕組み (Ventoy) が複雑だし、 そもそも ISO イメージを loopback デバイス経由でマウントして起動すると重くなるし、 必要なメモリ量も多いので、 利用したいとは思わなかった。 トラブった PC のスペックが低い場合など、 道具は軽ければ軽いほど好ましい。

というわけで、 exFAT でフォーマットした USBメモリに、 Live Ubuntu を入れる方法を考えてみた。 要は Live Ubuntu の initrd ファイル (cpio アーカイブ) をどう改変して exFAT に対応させるか?

基本的には Live Ubuntu の initrd を展開して exFAT に対応させる修正を行なった後、 cpio コマンドを使って initrd を作り直せばいいのだが、 initrd は複数の cpio アーカイブをつなげた形になっていて、 作り方によっては互換性の問題が起きるかも? Live Ubuntu 自身の update-initramfs コマンドを使って initrd を更新したほうが手軽だし確実。

なお、 (PC の起動に必要な) UEFI パーティションは (いまのところ) FAT でフォーマットしておいた方が無難と思われる。 ほとんど (全て?) の PC が exFAT や NTFS でフォーマットした UEFI パーティションを認識するらしいが、 最大限の互換性を求めるなら FAT にしておくべきだろう。

というわけで、 USBメモリの末尾 4MB (以下の実行例では sdc2) だけ FAT でフォーマットして UEFI パーティションとし、 残り (sdc1, ここでは 「boot パーティション」と呼ぶ) を exFAT でフォーマットする:

senri:/ # gdisk -l /dev/sdc
GPT fdisk (gdisk) version 0.8.4

Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.
Disk /dev/sdc: 121098240 sectors, 57.7 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 619FAAB5-5325-4404-99C4-F0541D53B069
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 121098206
Partitions will be aligned on 2-sector boundaries
Total free space is 0 sectors (0 bytes)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048       121090047   57.7 GiB    0700  Microsoft basic data
   2       121090048       121098206   4.0 MiB     EF00  EFI system partition
   3              34            2047   1007.0 KiB  EF02  BIOS boot partition
senri:/ # mkfs -t fat /dev/sdc2
mkfs.fat 4.0 (2016-05-06)
senri:/ # mkfs -t exfat /dev/sdc1
mkexfatfs 1.2.3
Creating... done.
Flushing... done.
File system created successfully.
senri:/ # 

UEFI パーティションは Windows ユーザ (あるいはコンビニのプリント機) からは見えないので、 boot パーティションのみを、 ふつうの USBメモリとして使うことになる。

次に grub-install コマンドで UEFI パーティションに GRUB (ブートローダ) をインストールする。 ついでに UEFI に対応していない PC (は滅多にないと思うが) でも起動できるように、 「--target i386-pc」オプションを使って MBR にも GRUB をインストールしておく。

senri:/ # mount /dev/sdc1 /mnt/usb
senri:/ # mount /dev/sdc2 /mnt/efi
senri:/ # grub-install --target x86_64-efi --efi-directory /mnt/efi --boot-directory=/mnt/usb/boot --removable
Installing for x86_64-efi platform.
Installation finished. No error reported.
senri:/ # grub-install --target i386-pc --boot-directory=/mnt/usb/boot --removable /dev/sdc
Installing for i386-pc platform.
Installation finished. No error reported.
senri:/ # blkid | grep sdc1
/dev/sdc1: UUID="E7F3-77CD" BLOCK_SIZE="512" TYPE="exfat" PTTYPE="dos" PARTLABEL="Microsoft basic data" PARTUUID="23041859-cc53-4164-be6a-44af6a966e5d"
senri:/ # 

UEFI パーティションに書込まれるのは GRUB コア (/EFI/BOOT/BOOTX64.EFI) のみで 124KB しかない。 UEFI パーティションは 4MB も要らないかも? GRUB 本体 (/boot/grub/x86_64-efi ディレクトリ) は boot パーティションに書込まれる。 /EFI/BOOT/BOOTX64.EFI は /boot/grub/x86_64-efi/core.efi の内容と同じ。

GRUB の設定ファイル /boot/grub/grub.cfg は ↓ こんな感じ:

set uuid="E7F3-77CD"
insmod all_video

menuentry "ubuntu 22.04.2 desktop amd64" {
  linux /casper/vmlinuz boot=casper uuid=$uuid
  initrd /casper/initrd
}

ここで "E7F3-77CD" は USBメモリの exFAT パーティション /dev/sdc1 の UUID (Universally Unique Identifier, 汎用一意識別子)。 blkid コマンドなどで調べることができる (上記 grub-install の実行例の末尾を参照)。

/casper ディレクトリは、 Ubuntu DVD (以下の実行例では /cdrom にマウントしている) からコピーする:

senri:/ # cp -a /cdrom/casper /mnt/usb/
senri:/ # ls -la /mnt/usb/casper
total 2813952
drwxr-xr-x 2 root root      32768 Feb 23 13:13 .
drwxr-xr-x 4 root root      32768 Jul  3 18:10 ..
-rwxr-xr-x 1 root root      59931 Feb 23 13:09 filesystem.manifest
-rwxr-xr-x 1 root root       2885 Feb 23 13:09 filesystem.manifest-minimal-remove
-rwxr-xr-x 1 root root       3578 Feb 23 13:09 filesystem.manifest-remove
-rwxr-xr-x 1 root root         11 Feb 23 13:09 filesystem.size
-rwxr-xr-x 1 root root 2731876352 Feb 23 13:09 filesystem.squashfs
-rwxr-xr-x 1 root root        833 Feb 23 13:12 filesystem.squashfs.gpg
-rwxr-xr-x 1 root root  137120699 Feb 23 13:09 initrd
-rwxr-xr-x 1 root root   12186376 Feb 23 13:09 vmlinuz
senri:/ # 

filesystem.squashfs が Live Ubuntu の root ファイルシステム。 これ以外の filesystem.* は不要なので削除して構わない。 vmlinuz が Linux カーネルで、 initrd がこれから書き換える initrd ファイル。

boot パーティションが FAT 等であれば、 この状態でブータブル USBメモリとして機能するが、 exFAT だと initrd が /casper/filesystem.squashfs を見つけられず Ubuntu を起動できない。

そこで initrd の内容を書き換えて exFAT を扱えるようにする。 といっても Linux Kernel 5.4 以降は (以前の exfat-fuse ではなく) カーネルレベルで exFAT を扱うことができる。 つまり必要なのはカーネルモジュール kernel/fs/exfat/exfat.ko を initrd へ追加することだけ。

まず Ubuntu DVD を用いて Live Ubuntu を起動する (インストール DVD から起動して「Ubuntu を試す」を選択)。 /etc/initramfs-tools/modules および /usr/share/initramfs-tools/scripts/casper-helpers に以下のパッチ ubuntu.patch をあてる:

--- etc/initramfs-tools/modules~	2023-02-23 12:59:33.000000000 +0900
+++ etc/initramfs-tools/modules	2023-07-03 08:46:22.000000000 +0900
@@ -9,3 +9,4 @@
 #
 # raid1
 # sd_mod
+exfat
--- usr/share/initramfs-tools/scripts/casper-helpers~	2022-05-30 23:40:38.000000000 +0900
+++ usr/share/initramfs-tools/scripts/casper-helpers	2023-07-03 08:48:03.000000000 +0900
@@ -36,7 +36,7 @@
     # FIXME: do something better like the scan of supported filesystems
     fstype="${1}"
     case ${fstype} in
-        vfat|iso9660|udf|ext2|ext3|ext4|btrfs|ntfs)
+        vfat|exfat|iso9660|udf|ext2|ext3|ext4|btrfs|ntfs)
             return 0
             ;;
     esac
@@ -234,7 +234,7 @@
             # will cause data loss when a live CD is booted on a system
             # where filesystems are in use by hibernated operating systems.
             case "$(get_fstype ${devname})" in
-                vfat)
+                vfat|exfat)
                     :;;
                 *)
                     continue;;
@@ -337,7 +337,7 @@
         for dev in $(subdevices "${sysblock}"); do
             devname=$(sys2dev "${dev}")
             case "$(get_fstype ${devname})" in
-                vfat|ext2)
+                vfat|exfat|ext2)
                     :;;
                 *)
                     continue;;
@@ -367,7 +367,7 @@
 is_supported_fs(){
     [ -z "${1}" ] && return 1
     case ${1} in
-        ext2|ext3|ext4|xfs|jfs|reiserfs|vfat|ntfs|iso9660|btrfs)
+        ext2|ext3|ext4|xfs|jfs|reiserfs|vfat|exfat|ntfs|iso9660|btrfs)
             return 0
             ;;
     esac
@@ -388,6 +388,7 @@
     modprobe xfs
     modprobe jfs
     modprobe vfat
+    modprobe exfat
     modprobe fuse
     [ "$quiet" != "y" ] && log_end_msg "...devs loaded..."
     touch /dev/.initramfs/lupin-waited-for-devs

この ↑ パッチでは、 起動時に (つまり initramfs で) 必要なカーネルモジュールを指定するファイル /etc/initramfs-tools/modules に 「exfat」 を追記している。 また、 initramfs 内のスクリプト 「/usr/share/initramfs-tools/scripts/casper-helpers」 を boot パーティションが exFAT でもエラーにならないよう修正している。

というか、 たったこれだけの修正で exFAT から起動できるのだから、 公式の Live Ubuntu で exFAT からの起動をサポートして欲しい。 exFAT を除外する理由でもあるのだろうか?

そして update-initramfs.distrib コマンドを使って新しい initrd を生成する。 ここで (通常の Ubuntu と同じ感覚で) update-initramfs を使ってしまうと 「update-initramfs is disabled since running on read-only media」 と言われてしまうので注意。 「read-only media」 だからダメなのではなく、 Live Ubuntu の update-initramfs は、 このメッセージを出力するだけの sh スクリプトに置き換えられている (いったい何のために?)。

例えば Live Ubuntu の Terminal を使って以下のように実行する:

root@ubuntu:/# wget https://www.gcd.org/sengoku/docs/ubuntu-22.04.2-desktop-amd64.patch
--2023-07-04 01:47:50--  https://www.gcd.org/sengoku/docs/ubuntu-22.04.2-desktop-amd64.patch
Resolving www.gcd.org (www.gcd.org)... 71.19.146.203, 74.207.241.21, 219.94.252.139, ...
Connecting to www.gcd.org (www.gcd.org)|71.19.146.203|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1799 (1.8K) [text/plain]
Saving to: ‘ubuntu-22.04.2-desktop-amd64.patch’

2023-07-04 01:47:57 (38.1 MB/s) - ‘ubuntu-22.04.2-desktop-amd64.patch’ saved [1799/1799]

root@ubuntu:/# patch -p0 < ubuntu-22.04.2-desktop-amd64.patch
patching file etc/initramfs-tools/modules
patching file usr/share/initramfs-tools/scripts/casper-helpers
root@ubuntu:/# uname -a
Linux ubuntu 5.19.0-32-generic #33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 30 17:03:34 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
root@ubuntu:/# update-initramfs.distrib -c -k 5.19.0-32-generic
update-initramfs: Generating /boot/initrd.img-5.19.0-32-generic
cryptsetup: ERROR: Couldn't resolve device /cow
cryptsetup: WARNING: Couldn't determine root device
W: Couldn't identify type of root file system for fsck hook
root@ubuntu:/# 

生成された /boot/initrd.img-5.19.0-32-generic を USBメモリの boot パーティションの /casper/initrd へコピーする。

以上で、 Ubuntu が起動できる exFAT な USBメモリができた。 ふだんは普通の USBメモリとして使える。 /casper/filesystem.squashfs は 2605MB もあるが、 いつでも Ubuntu DVD からコピーすることで元に戻せるので、 USBメモリに空きがないときは気軽に削除して構わない。

Filed under: システム構築・運用 — hiroaki_sengoku @ 07:55
2023年6月12日

Wio Node (ESP8266) に MicroPython をインストールして、Wi-Fi 照度&人感センサを作ってみた

市販されている IoT 機器は、 いつサービス終了してしまう (クラウドサーバ停止で使えなくなる) かも知れず、 そのたびに API を解析するのは現実的ではないなぁと思っていたところ、 MicroPython (日本語) を使えば手軽に IoT 機器を自作できることに気付いた。

例えば部屋の明るさを Wi-Fi 経由でサーバへ通知する照度センサは、 こんな感じで書ける:

import machine
import socket
import network_cfg

pwr = machine.Pin(15, machine.Pin.OUT)
pwr.value(1)

def webhook(v):
    addr = socket.getaddrinfo('gt.gcd.org', 12345)[0][-1]
    s = socket.socket()
    s.connect(addr)
    s.send('{"magic":"'+network_cfg.MAGIC+'","type":"Illuminance", "Darkness":"'+str(v)+'", "DeviceName":"wionode"}\n')
    s.close()

adc = machine.ADC(0)

def illumi(p):
    darkness = adc.read()
    try:
        webhook(darkness)
    except OSError as exc:
        print("exception\n")

tim = machine.Timer(-1)
tim.init(period=60000, callback=illumi)

webhook(v) はサーバ (この例では gt.gcd.org のポート 12345番。 LAN 内のサーバなので外部からはアクセスできない) へ通知する関数。 「adc.read()」で照度センサの値 (0 〜 1024) を読み取って、 webhook(v) でサーバへ通知するだけ。 タイマを使って 60秒ごとに illumi 関数を実行して、 その時点の照度をサーバへ通知するようにしてみた。

C などのコンパイル型言語と違って、 Python だから対話的にコード片を実行してみることができて (REPL)、 とりあえず動くプログラムを書くだけなら、 あっと言う間に書ける。 Arduino のスケッチみたいに、 いちいち 「コンパイルしてボードへ書込んで実行」 を何度も何度も繰り返すのと比べると、 REPL は圧倒的に楽。 IoT 機器だと実行速度はさほど要求されない (ことが多い) わけで、 REPL 一択だと思う。

いわゆる 「ラズパイ電子工作」 だとブレッドボードで配線するのが定番で、 ブレッドボードだから配線を手軽にできるのはいいのだけど、 IoT 機器として実地に使おうとすると電子部品が剥き出しのままでは困るし、 ケースにいれると大きすぎて設置方法に困る。

WioNode + PIR & Light Sensor

その点、 Wio Node (Seeed Studio 102110057) は、 極めてコンパクトなのに Grove コネクタが 2個ついているので、 Grove シリーズと互換なセンサ類をつなぐだけ。 電子部品の出っ張りとかが無いので、 ケースにいれなくてもそのまま使えそう (というか実際、このままで実地に使っている)。

Wio Node の 2個の Grove コネクタに、 照度センサと人感センサをつないでみた。 写真中央左寄りが M5Stack用 光センサユニット (517円) で、 写真右上が HC-SR312 (AM312) 人体検知センサ (1個 166円)。 Wio Node (写真下) は私が買ったとき (2021年11月) は 1250円だったのに、 急に値上がりして今は 1700円くらい。

値上がりしてしまったとはいえ、 合計 2400円くらいで Wi-Fi 通信する照度&人感センサが作れてしまう。 いつサービス終了してしまうか恐れながら市販の IoT 機器を使うよりずっといい。

Grove コネクタは 黄 白 赤 黒 の 4ピン。 黄と白は信号線で、 赤は電源 (5V or 3.3V)、黒は GND。 Seeed studio オリジナルの Grove ケーブルは 1番ピンが黄色 (写真左側) だけど、 M5Stack などの互換ケーブルだと 1番ピンが白色で 白 黄 赤 黒 の順番 (写真右側)。

オリジナルと互換ケーブルとでは黄と白が入れ替わっているが、 違うのは色だけで互換性は配慮されている (と思う)。 例えば、 アナログ出力の Grove センサのほとんど (全て?) は、 1番ピンがアナログ信号線となっている。

3番ピン(赤) は、 Wio Node の場合 3.3V が供給されるが、 M5Stack や Arduino だと 5V のものが多いので注意を要する。 例えば人感センサの定番 HC-SR501 (1個 136円) の電源電圧は 5V 〜 20V なので Wio Node の Grove コネクタから電源を供給することはできない。

More...
2022年5月11日

WSL2 (Windows Subsystem for Linux 2) で物理ディスク上の独自 OS を動かしてみた

Windows で VMware Workstation Player を使って Linux ベースの独自 OS (以下 GCD OS と呼ぶ) を動かしていたのだけど、 WSL2 (Windows Subsystem for Linux 2) が物理ディスクをマウントできるようになったと聞き、 WSL2 が VMware の代りに使えるか試してみた。 ただし、物理ディスクをマウントするには Windows 11 ビルド 22000 以上が必要。

本当は Linux カーネルも自前でビルドしたものを使いたいが、 とりあえず WSL2 のカーネルをそのまま使い、 root ファイルシステムは物理ディスク上にインストールしてある GCD OS を使う方向で考えた。

以前、 OpenVZ な VPS サービス上で GCD OS を動かしたことがある (10年前!) ので、 その時と同様に chroot して物理ディスク上の GCD OS を起動するのが簡単そう。 つまり、 以下のシェルスクリプト gcd.sh を WSL2 上で実行するだけ:

#!/bin/sh
PATH="/mnt/c/WINDOWS/System32:/usr/bin:/bin:/usr/sbin:/sbin"
ROOT="/usr/local/GCD"

cd /mnt/c
wsl.exe --mount '\\.\PHYSICALDRIVE1' --bare

test -d $ROOT || mkdir $ROOT
mount LABEL=/ $ROOT

$ROOT/etc/init.d/chroot

このスクリプトは WSL2 の仮想ディスク (Virtual Hard Disk) 内に置いてもよいが、 私は Windows の C: ドライブに置いた。 WSL2 の内容を変更する必要がなくなるし、 WSL2 でどのディストリビューションを使っているかに依存しなくなる。 仮想ディスクが肥大化したら、インストールし直して初期状態に戻してもよい。

例えば C:\bin\gcd.sh に置いて、 次のように実行する:

C:\Windows\System32\wsl.exe -u root -- /mnt/c/bin/gcd.sh

タスクスケジューラーで Windows の起動時に自動的に実行すれば、 Windows にログインしなくても外部から ssh で GCD OS へログインできるので便利。 VMware より軽くて手軽で便利かも?

以下、このスクリプト gcd.sh を順に説明する:

まず、 「wsl.exe --mount '\\.\PHYSICALDRIVE1' --bare」 で WSL2 から物理ディスク 「PHYSICALDRIVE1」 が見えるようにする。 wsl.exe は Windows のコマンドだが、 WSL2 は Linux なのに /mnt/c にマウントされた C: ドライブ上の Windows のコマンドが実行できる (Linux カーネルの binfmt_misc を使っている)。

(Linux の) lsblk コマンドを使うと、 物理ディスクが見えることが確認できる:

Linux 5.10.16.3-microsoft-standard-WSL2.
kayano:~ $ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda      8:0    0   256G  0 disk
sdb      8:16   0 339.7M  1 disk
sdc      8:32   0   256G  0 disk /mnt/virtual
sdd      8:48   0   7.3T  0 disk
├─sdd5   8:53   0   200G  0 part 
├─sdd6   8:54   0    16G  0 part [SWAP]
└─sdd7   8:55   0   6.8T  0 part /

sdd が WSL2 から見えるようになった物理ディスク (8TB HDD)。 パーティションが 3つあることが分かる。 うち sdd7 が GCD OS の root ファイルシステム。

一点注意すべきなのは、 Windows のシステムドライブ 「PHYSICALDRIVE0」 は指定できないので、 システムドライブとは異なる物理ディスクを使う必要があるという点。 8TB HDD など大容量のハードディスクを、 パーティションで区切って Windows と Linux の両方をインストールしている人は多いと思うのだけど、 残念ながら Windows と同じディスク上にある Linux パーティションは、 WSL2 から使うことはできない。 VMware ならできるのに...

仕方がないので私は (わざわざ) M.2 NVMe SSD を買って Windows のシステムドライブを NVMe へ移し、 SATA0 につないだ 8TB HDD を (Windows をインストールしていたパーティション 1〜4 を削除して) Linux 専用にした。

また、WSL2 上で wsl.exe を実行するとき、 カレントディレクトリが WSL2 の仮想ディスクだと、 どんな引数でも常に 「Invalid argument」 エラーになる:

root@kayano:~# /mnt/c/Windows/System32/wsl.exe --shutdown
/mnt/c/Windows/System32/wsl.exe: Invalid argument
                                                 root@kayano:~#

エラー出力後の改行がうまくいかないあたり、 いかにもバグっぽい?

カレントディレクトリが C: ドライブだと正常に実行できるようなので、 wsl.exe を実行する前に 「cd /mnt/c」 を行っている。

物理ディスクが WSL2 で見えるようになったら、 次にこの物理ディスクをマウントする: 「mount LABEL=/ $ROOT」。 ここでは LABEL が 「/」 のパーティションを 「$ROOT」 つまり /usr/local/GCD へマウントしている。

あとは GCD OS を起動するだけ: 「$ROOT/etc/init.d/chroot」。 /etc/init.d/chrootOpenVZ 上で GCD OS を起動するときも使った、 以下のシェルスクリプト:

#!/bin/sh
root=`echo $0 | sed -e 's@/etc/init.d/chroot$@@'`
if [ ! -d $root ]; then
   echo "Can't find root: $root"
   exit 1
fi
sed -n 's/^[a-z][_a-z]* \([^ ][^ ]*\) .*/\1/p' < /proc/mounts | while read d; do
    if [ -d "$d" ]; then
	test -d "$root$d" || mkdir "$root$d"
	mount -obind "$d" "$root$d"
    elif [ -f "$d" ]; then
	test -f "$root$d" || touch "$root$d"
	mount -obind "$d" "$root$d"
    fi
done
if [ -d /lib/modules/`uname -r` ]; then
    mount -obind /lib/modules $root/boot/lib/modules
fi
chroot $root /bin/sh <<EOF
swapon -a
mount -a -t ext4
/etc/init.d/svscanboot &
/etc/rc.d/rc.M
EOF

このシェルスクリプトは、 まず WSL2 の /proc/mounts を参照して、 WSL2 がマウントしているディレクトリとファイルを、 そのまま GCD OS のルート (/usr/local/GCD) へマウントする。 これで GCD OS でも /mnt/c にマウントされた Windows コマンドを実行できるようになる。

次に 「chroot /usr/local/GCD /bin/sh」 を実行して、 chroot 環境下で svscanboot/etc/rc.d/rc.M を実行する。 svscanboot は daemontools の起動スクリプト。 GCD OS のほとんどのデーモン類は daemontools の管理下で起動される。 一方 /etc/rc.d/rc.M は、 GCD OS のブートスクリプトで、 ネットワーク等の各種設定と、 一部のデーモン類の起動を行なう。

ssh サーバや WWW サーバも /etc/rc.d/rc.M が立ち上げるが、 いままで WSL2 のネットワークは Windows 内でしか見えないバーチャルネットワーク (内部ネットワーク) だったらしい。 「WSL2 外部から接続」 などとググっても、 外部から WSL2 の ssh サーバにログインするには (netsh.exe の) portproxy を使え、という話ばかり出てくる。

Hyper-V Virtual Switch

どのバージョンから可能になったのかは知らないが、 「仮想スイッチ マネージャー」 で設定すれば VMware のようなブリッジモードが WSL2 でも使えるようになる。

まず Windows 管理ツールの 「Hyper-V マネージャー」 を実行する。 仮想マシン (この例では KAYANO) を選択し、 「操作(A)」 メニューから 「仮想スイッチ マネージャー(C)...」 を選ぶと、 「仮想スイッチ マネージャー」のウィンドウが開く。

左ペイン 「仮想スイッチ」 の中から 「WSL」 を選び、 右ペインに表示される 「接続の種類」 として 「外部ネットワーク(E):」を選択し、 適切なネットワーク アダプターを選択する。

これで WSL2 が外部ネットワークと通信できるようになる。 (いまのところ) タグVLAN が使えるようにはできていないが、 VMware ではタグVLAN が使えるので、 なんとかしてタグVLAN が使えるようにしたいところ...

他のマシン (以下の例では senri) から ssh で GCD OS (kayano) へログインしてみる:

senri:/ # ssh kayano
Last login: Wed May 11 04:16:34 2022 from senri.gcd.org
Linux 5.10.16.3-microsoft-standard-WSL2.
kayano:~ # ip addr show dev eth0
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:15:5d:ff:11:b1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.18.40/24 brd 192.168.18.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:feff:11b1/64 scope link
       valid_lft forever preferred_lft forever
kayano:~ # tcpspray senri
Transmitted 102400 bytes in 0.000315 seconds (317460.317 kbytes/s)
kayano:~ # free
             total       used       free     shared    buffers     cached
Mem:       8055704     573892    7481812          0      30704     178088
-/+ buffers/cache:     365100    7690604
Swap:     18874364          0   18874364
kayano:~ # chroot_escape /bin/bash
groups: cannot find name for group ID 11
groups: cannot find name for group ID 14
root@kayano:/# lsb_release -d
Description:    Ubuntu 20.04.4 LTS
root@kayano:/# exit
kayano:~ #

chroot 環境から脱出 (chroot_escape /bin/bash) すると WSL2 の Ubuntu 環境に戻る。 で、exit すると GCD OS に戻る。

この GCD OS で物理ディスクの読み書き速度を測ってみた:

kayano:/ # hdparm -t /dev/sdd7

/dev/sdd7:
 Timing buffered disk reads:  538 MB in  3.01 seconds = 178.93 MB/sec

kayano:/ # dd if=/dev/zero of=/tmp/test bs=1024k count=10240
10240+0 records in
10240+0 records out
10737418240 bytes (11 GB, 10 GiB) copied, 60.5833 s, 177 MB/s

読み書きともに 177MB/sec くらい。 Core i5-8500 マシンだとこんなもの? 同じ PC (同じ Windows) で VMware 上の同じ GCD OS でも測ってみると、 読込みが 180.62 MB/sec で書き込みが 132 MB/s だった。 簡易な測定なので、ほぼ同等と言っていいと思う。

ちなみに仮想化なしで直接このマシン上で測ると、 読込み 178.65 MB/sec 書込み 233 MB/s なので、 仮想マシンによるオーバーヘッドは多少あるようだ。 とはいえ AMD FX-4100 マシンとかだと読込み 174.82 MB/sec 書込み 95.3 MB/s なので、 実用上は 177MB/sec もあれば充分?

AMD FX とかの 10年前のマシンだと CPU がボトルネックになってる感じ。そろそろ 「Sandyで十分おじさん」 は卒業すべき?

ネットワークの速度も測ってみた:

kayano:/ # iperf3 -c esaka
Connecting to host esaka, port 5201
[  5] local 172.17.14.235 port 35504 connected to 192.168.18.20 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec   116 MBytes   970 Mbits/sec    0   3.01 MBytes       
[  5]   1.00-2.00   sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
[  5]   2.00-3.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   3.00-4.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   4.00-5.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   5.00-6.00   sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
[  5]   6.00-7.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   7.00-8.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   8.00-9.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   9.00-10.00  sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  1.10 GBytes   943 Mbits/sec    0             sender
[  5]   0.00-10.05  sec  1.10 GBytes   938 Mbits/sec                  receiver

iperf Done.

1Gbits/sec の LAN なので、ほぼ上限の速度が出ている。 同条件で VMware でも測ってみると、 送信が 928 Mbits/sec で受信が 925 Mbits/sec だった。 仮想化なしでも、 送信が 943 Mbits/sec 受信が 941 Mbits/sec なのでほとんど同じ。

というわけで、 速度的にはディスクもネットワークも問題無さそう。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 09:37
2022年3月9日

所得税を分割して、複数の高還元率クレジットカードを何回も使って納付してみた hatena_b

所得税の納付は、 確定申告書等作成コーナー経由だと全額一括払いになってしまうが、 国税クレジットカードお支払サイトを直接アクセスすることで、 任意に分割して支払うことができる。

この「お支払サイト」は、 氏名、住所、電話番号、納付先税務署、納付税目 (申告所得税及復興特別所得税) を入力した上で、 クレジットカードで任意の金額を支払う仕組みになっている。 それぞれの納税者が合計いくら納付済みか集計するには、 納税者と一対一に対応する ID (識別番号) が必要だが、 ID の入力は求められない。 つまり支払うだけの納付専用サイト。

e-Tax を使って確定申告を行った納税者なら e-Tax の 「利用者識別番号」が ID になるが、 e-Tax を使わなくても確定申告はできるわけで、 「お支払サイト」としては ID 無しで納付できる仕組みにするしかないのだろう。

この仕組みだと税務署側で、 「お支払サイト」 から送られてきた納付データと、 確定申告のデータを突合する作業が必要になる。 やっぱり手作業? DX への道は遥か遠い。 個人番号 (マイナンバー) を ID として入力させればいいのにねぇ...

ID とは言えないものの各税務署が「整理番号」を発行していて、 この「お支払サイト」でも「整理番号」を入力することができる。 整理番号の入力は必須ではないが、 入力しておくことで税務署の人たちの突合の手間を減らし、 間違いが起きるリスクを減らすことができるだろうから、 自分の「整理番号」を調べて入力したほうがヨサゲ。

Tax Payment Statement

整理番号は、 e-Tax のメッセージボックスに届く 「確定申告等についてのお知らせ」に記載されている他、 毎年 1月下旬に郵送で届く「確定申告のお知らせ」 「確定申告の納付書」や、 還付金がある場合は「国税還付金振込通知書」にも記載されている。

所得税をクレジットカードで支払う場合、 税額 1万円につき決済手数料 76円+消費税がかかる。 例えば所得税が 99万1円〜100万円の範囲なら税込 8,360円 (つまり 0.84%) もの決済手数料を払う必要があるわけで、 決済手数料より高いポイント等がもらえる高還元率クレジットカード (還元率が 1% 以上) でないと意味がない。

ところが高還元率クレジットカードは、 ポイント等の還元率が高いいっぽう還元を受けられる上限額が低いことが多い。 例えば「Visa LINE Pay クレジットカード」(以下「LINEカード」と略記) は、 支払額の 2% の LINE ポイントがもらえるが、 税金/保険において、1回あたりの支払につき 5万円を超える分は、 ポイント還元の対象外となる。

また、ソニー銀行が発行する「Sony Bank WALLET デビット付きキャッシュカード」 (以下「Sonyカード」と略記) は、 Club S のステータスに応じて最大 2% のキャッシュバックがもらえるが、 寄付、納税、公共料金などの支払ではキャッシュバック合計額が最大 1万円/月までとなる。

どちらの高還元率カードも、 所得税を分割して納付することで上限額を超えてしまうことを回避できる。 LINEカードの場合は 1回 5万円ずつ複数回に分けて払えば 2% のポイントが得られるし、 Sonyカードの場合は (確定申告を 2月に行えば) 50万円ずつ 2月と 3月の 2回に分けて支払うことで各月 2% 満額 1万円のキャッシュバックが得られる。

例えば 140万円の納税を行う場合なら、 2月と 3月に Sonyカードで 50万円ずつ納付し、 3/15 までに LINEカードで 5万円ずつ 8回納付すれば良い。

実際の納付では決済手数料がかかるので、 Sonyカードでは納付額 495,820円と決済手数料 4,180円を、 LINEカードでは納付額 49,582円と決済手数料 418円を払うことになる。 ここで注意すべきなのは決済手数料が 1万円単位で、 1万円に満たない端数は切り上げられるという点。 分割の仕方によっては余計な決済手数料を払うことになってしまう。

例えば所得税額が 12万300円だった場合、 一括で納付すれば決済手数料は税込 1,086円 (=13*76*1.1) だが、 3等分して 4万100円ずつ納付すると決済手数料は税込 1,254円 (5*76*1.1 = 418円ずつ 3回) になってしまう。 3回に分けて納付したときの決済手数料を一括納付の場合と同額にするには、 まず 4万300円を納付 (決済手数料 5*76*1.1 = 418円) し、 残り 2回は 4万円ずつ納付 (決済手数料は 4*76*1.1 = 334円ずつ) すればよい。

というわけで、 所得税 1,390,400円を 9回に分けて 5枚のクレジットカードで納付してみた。 決済手数料は計 11,702円なのに対し、 キャッシュバックやポイント等で計 28,241円の還元を得た。 差し引き 16,539円の利益。

納付日税額手数料支払額カード還元額
2022-02-19495,820円4,180円500,000円Sony10,000円
2022-02-1949,582円418円50,000円LINE1,000円
2022-02-2479,996円668円80,664円Diners403円
2022-02-2540,000円334円40,334円LINE806円
2022-03-0240,000円334円40,334円LINE806円
2022-03-03100,000円836円100,836円SMBC NL3,025円
2022-03-0749,182円418円49,600円LINE992円
2022-03-08495,820円4,180円500,000円Sony10,000円
2022-03-0840,000円334円40,334円Toyota1,209円
合計1,390,400円11,702円1,402,102円28,241円

LINEカードでの支払は、 同じ納税を何度も繰り返すとポイント付与を止められるかも?と思って、 ポイント付与を確認して (さらに LINE証券へ送金して) から次の支払を行った。 幸い今回は 2% 満額の LINE ポイントを得たが、 分割納付する人が増えるとポイント付与ルールが改訂されるかもしれない。 もっとも、2% 還元自体が 2022年4月30日までであり、 5月以降どうなるか不明。

「三井住友カード ゴールド(NL)」(上表では「SMBC NL」と略記) は、 「最大30,000円相当の XRP交換券 プレゼントキャンペーン」(4月30日まで) を行っているので、 支払額の 3% 相当の XRP (暗号資産) が付与される。 また、 このカードには 「年間100万円のご利用で翌年以降の年会費永年無料」特典があり、 この納税で年間利用額 100万円を達成できた。

「ダイナースクラブカード」(上表では「Diners」と略記) は、 この納税で年間利用額が 60万円に達したので、 次年度の年会費が無料になる。 ポイント還元率はわずか 0.5% だが、 24,200円の年会費が無料になるのは大きい。

「TOYOTA Wallet」(上表では「Toyota」と略記) は、 物理的なカードが無いバーチャルカードで、 他のクレジットカード等から残高をチャージして使う。 残高の上限は 5万円なので 5万円を超える支払には利用できない。 実店舗では iD または Mastercard コンタクトレス (Apple Pay のみ) として使える。 支払額の 1% が残高にキャッシュバックされる。

オンライン支払では、 カード名義が 「TOYOTAWALLET MEMBER」 固定なので、 利用できないサイトも多いが、 そもそもこの 「お支払サイト」 はトヨタファイナンス(株)が運営するサイトなので、 同じトヨタファイナンスなのだから使えるかも?と思って試してみたら、 あっさり納付完了してしまった。

残高のチャージを高還元率カード、例えば Sonyカードで行えば、 チャージの際に 2% のキャッシュバックが得られるので、 TOYOTA Wallet のキャッシュバックと合わせて 3% の還元率となる。 上限が同じ 5万円なら、 LINEカードでなく最初から TOYOTA Wallet を使えばよかった。(^^;

Sonyカードはクレジットカードではなく Visaデビットだが、 決済代金が即引き落とされること以外はクレジットカードと変わらない。 もちろん 3Dセキュアに対応している。 高還元率カードとしては群を抜いている (しかも還元はポイントではなくキャッシュバック!) と思うのだけど、 イマイチ認知されていないのは何故だろう?

Filed under: 経済・投資・納税 — hiroaki_sengoku @ 08:15
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
Older Posts »