ほかにもやりたいことがあるのだが、ここ数ヶ月やってることがなかなか「これでよし」とならなくて、とりかかれない。ぶっちゃけそろそろ飽きてきたのでさっさと日記に書きだして一旦おわりにしたい。
✖
|
ほかにもやりたいことがあるのだが、ここ数ヶ月やってることがなかなか「これでよし」とならなくて、とりかかれない。ぶっちゃけそろそろ飽きてきたのでさっさと日記に書きだして一旦おわりにしたい。
ここ数ヶ月ぐらいキーボードを作っていた。そのためにいろいろ yak-shaving としかいいようがないことも多々していた。
いろいろ書くことが多いので、細かい設計などについては別途エントリを分ける。
あたりをそれぞれ別途詳細なエントリを書く。だいたいの人は細かいことはどうでもいいと思うので、概要のみこのエントリにまとめる。
UNIX ベースのキーレイアウト (というかHHKBをベース) とし、違和感なしに分割キーボードとする。
FOTA/DFU (Firmware On The Air / Device Firmware Update) かつオンラインコンパイラによりキーカスタマイズのために環境構築が不必要。あるいは MK20 による書きこみなので、3ピンの配線でUSBからマスストレージクラス経由で書きこみ可能。
UNIX配列のBluetoothキーボードというのが希少で前々から欲しいと思っていたので、接続インターフェイスは Bluetooth としてみた。
インターフェイスとして RedBear BLE Nano というのを使うことにした。国内技適にも通っており結構安い。mbed が開発環境に使える。名前の通り BLE (Bluetooth 4.0) 接続になる。ただ、なんというか、この選択(無線化)は割と悪夢の始まりだった。
BLE Nano はピン数が少ないため、I2C GPIO 拡張の MCP23017 を2つ使っている。消費電力削減のため GPIO の割込みを活用している。キーの部分は単にキーマトリクスなので特におもしろいところはない。
ステータスLEDを1つだけつけている。これはペアリングステータスを示すため。キーボードにLEDあっても見ることないし消費電力の無駄なのでほとんど消灯させておく。
KiCAD のプロジェクトは github に置いてあります。https://github.com/cho45/Keble
PCB Milling (CNCフライスによる基板切削) でやることを前提としたので、片面基板+最低限のジャンパで構成した。
複雑ではないが配線数は多いので、片面という制約をつけると結構厳しい。がオートルータでなんとかなった。製作しやすくするため、デザインルールで最小幅を0.3mmとした。
とりあえず左を作ってファームウェアと共に実装を検証し、それから右側を作った。そのため、右に比べると左側のクオリティが明らかに低い。
設計がちゃんとできている前提で、無心ではんだ付けをするだけ。振動とたわみの負荷がSMDにかかるのがなんとなく嫌で全てリード部品としたため、特に難しいところはない。
基板以外に、バックプレート(2mm)とフロントプレート(1mm)をさらにプラ版から切り出している。なので、3層構造になっている。側面はない。
mercurial:
hg clone https://developer.mbed.org/users/cho45/code/keyboard/
前述の通りだけど、今回は mbed のオンラインコンパイラで全て実装した。BLE Nano + mbed でセキュアペアリングして HID デバイスとして動かす例なんてのは例がさっぱりなくて大変苦労した。
HID キーボードとして簡単に動かすぐらいまでは、既にやってる人がいるので難しくない。しかし、実用キーボードとして安定して動かすようにするまでがかなり辛かった。
70%ぐらいの完成度まではすぐできるけど、そこから90%ぐらいまで完成度を上げるには大変な労力がいる。ハマったポイントが蓄積されていて、使えるぐらいに安定して動くコードがある状態にできたので、まぁ良かった…… ハードウェアよりもファームウェアの実装の知見のほうが遥かに価値があると思う……
基本的に mbed 環境でがんばるってのが筋が悪いのだけど、なんとなくオンラインコンパイラにこだわって意固地になって苦労している感じ。
主観的には90%といったところと思ってる。温度感としては「ブログ書くぐらいなら全く問題がなく、仕事で使うのにもまぁまぁ使える」ぐらい。
仕事で1日使ったところ、数回再接続(約5〜10秒ぐらい)が必要になったのと、1度完全に刺さった(WDTで復帰せず、リセットで復帰)。0dBmで送信しているので、普通の距離なら物理要因で接続が切れることはないと思うので、他の再接続もファームウェアのバグだと思うが原因不明。
自分の主観的には割と稀ぐらいまできて普通に使える、バリバリ集中してコード書きまくる人だとイライラするかもしれない。
キーマップが HHKB と同じ (Fnキーによる矢印キーも実装してある。レイヤーってやつ)なので、基本的にどっちも全く違和感なく使える。
キーキャップ2セット買ってるがメタキー用の無刻印のものと通常キーの刻印ありのものを分けたかったからで、1セット+必要なキーだけとかならもっと安くはできるはず。いずれにせよキーキャップの原価支配率が高い。これだけ買ってるのに右シフトキーサイズのキーが Caps Lock しかなくて、しかたなく代用している。
基板切削を自力でやっているので注意がいる。もし基板を外注するなら、左右別のデザインにするとサイズ的に5枚組み1万ぐらいはかかるはず。とはいえ、外注しても2〜3万ぐらいの原価なので、趣味で作るのはまぁまぁ現実的といえる。基板をシェアするともだち(笑)がいるならもうちょっと安くて楽をできる。(趣味で、というのは自分の作業コストをゼロとして見積るという意味です)
ヤパチーで ErgoDox を見て触発されて、ErgoDox について調べた(買わないけど) | tech - 氾濫原 このエントリを書いた時点でさいきょうのキーボード自作の実現性について考えていたので、そこから約1ヶ月半ぐらい。ebay や aliexpress が部品調達のメインで、かなり待ち時間があるので、実働は1ヶ月ぐらいかな。休日も平日も夜中にしか開発できないので、やる気があれば半月ぐらいで形になりそう。主観的には(大変すぎて)3ヶ月ぐらいずっとやってるつもりだったけど、案外早くできたっぽい……
ぶっちゃけ1ヶ月ぐらいじっくり取り組むと飽きる。
キーボード自作は結構面白い。というか奥が深いと感じる。自分が一番よく触るインターフェイスを自分である程度作れるというのは満足度が高い。
いきなりイチから盛り盛りで作ったわりには良くできたと思うが、特にハードウェア部分は製品レベルではない。自分で作って自分で使うぶんには十分といえる。
「さいきょうのきーぼーど」追い求めると沼にハマるので、ほどほどにしたほうがよさそう。
フルキーボード作るのは結構コストがかかるので、既存のキーボードと組合せる前提で、カーソルキーだけとか、ファンクションキーだけ、みたいな無線キーボードなら安く作れて良さそう。そして十分実用にできると思われる。
BLE Nano は書きこみ器セットで購入しても $32.90 とかなりお得。BLE Nano Kit - Product 単体 なら $17.90。RedBear は香港みたい。公式の通販から最低送料のオプションで買っても割とすぐ届く。
ただのビーコンとして使うには高価に感じるかもしれないが、PCとの低消費電力無線通信デバイスでこの価格帯のものは殆どない。
そしてARM かつ、mbed を開発環境につかえる。なお Arduino からも書きこめる。ナイス。
載っている BLE モジュールも nRF51822 という界隈でデファクトスタンダートみたいなやつなので比較的情報が豊富。
そして小さい。小さいのは正義。その分IOは少ないが割となんとかなる。
0x3d はMessage Integrity Check (MIC)が失敗した、というエラーらしい。ホスト側で発生する。デバイス側から送られてきたメッセージのセキュリティチェックエラーのようだ。
ということで、デバイススタックの SoftDevice のバグでは? と思うところだけど、そうではないらしい。どうやら mbed と相性が悪いらしい。
0x3d が出るのは最初はファームウェアのバグでどこかが stuck しているからだと思っていた。しかしどうも stuck していなくても 0x3d が起こることがある。で、そろそろ Nordic スタックのバグを疑ってみる。しかしそこのバグなら既にハマっている人がいるはずなのでググる。
すると Implement BLE security · Issue #44 · lancaster-university/microbit-dal · GitHub あたりに Nordic のバグじゃね? みたいな話がでてくる。そこからサブイシューがつくられている。
MIC failures observed with secure BLE · Issue #61 · lancaster-university/microbit-dal · GitHub で
Problem is caused by mbed-classic disabling interrupts when a timer interrupt is triggered. This was too long for the underlying BLE stack to code with during critical radio events.
と原因と、さらに解決策がいくつか示されている。一番簡単なのが3つめなので、このプロジェクトでは3つめが採用されたっぽい。しかし肝心のコミットへのリンクはないので頑張って探す。
Merge branch 'secure-ble' · lancaster-university/microbit-dal@3c31479 · GitHub このコミットをつらつら眺めていくと、どうやら対策コードっぽいものが見つかった。
// configure the stack to hold on to CPU during critical timing events.
// mbed-classic performs __disabe_irq calls in its timers, which can cause MIC failures
// on secure BLE channels.
ble_common_opt_radio_cpu_mutex_t opt;
opt.enable = 1;
sd_ble_opt_set(BLE_COMMON_OPT_RADIO_CPU_MUTEX, (const ble_opt_t *)&opt); ここで SoftDevice の API を呼んで、無線アクティビティがあるときはCPUを完全にブロックしてアプリケーションの実行を止めるらしい。簡単なコードだ。
コピペしてみるとエラーの発生がなくなった。まじ良かった……
Apple が提供している Bluetooth Explorer の Event Log を開きっぱなしにしておけば、接続情報について常にデバッグログに残る。
仕様に書いてあるが標準だと 0.5mA になっている。これは超高輝度LEDなら直接光るかもしれない。電源電圧がそもそも低いので青色とかはやめたほうがよさそう。
なお、設定を変えると最大3ピンまで5mAのドライブ能力に拡張することができる。これは特にピンの制約はなくて、どのピンでも可能なようだ。LEDぐらいしか駆動するものがないのなら、LED ピンのドライブ能力を拡張しておくと安心できる。
設定方法は例えば以下の通りで、
NRF_GPIO->PIN_CNF[PIN_STATUS_LED] = (NRF_GPIO->PIN_CNF[PIN_STATUS_LED] & ~GPIO_PIN_CNF_DRIVE_Msk) | (GPIO_PIN_CNF_DRIVE_H0H1 << GPIO_PIN_CNF_DRIVE_Pos);
PIN_CNF の DRIVE を変更すれば良い。この例だとソースもシンクも5mAになる。ソースとシンクは別々に設定可能。
なお mbed 環境で DigitalOut とかしている場合 PIN_CNF レジスタは変更済みなので、必要ないところは上書きしないように注意する必要がある。
DFUService というのが mbed の BLE_API だと提供されていて勘違いしたけど、これは実際のファームウェア書きこみ処理は一切行わない。これがやっていることは bootloader を起動するということだけだ。
FOTA の仕組みとしては
という感じですすむ。
まず、コンパイル済みの bootloader が必要で、これを USB 経由で書きこむ。これで準備完了になるので、これ以降は FOTA だけで書く必要がある。USB 経由で書きこむと bootloader を上書きしてしまうので、FOTA は無効になる 。
https://github.com/RedBearLab/nRF51822-Arduino/tree/S130/bootloader
Arduino IDE 経由で書きこめる bootloader になっているが、FOTA の機能もついている。BLE Nano だとこれ使っておけば良さそう。他のも使ってみたがこれだけ動いた。
ただ、上記 bootloader.hex を書きこんでも、mbed のオンラインコンパイラでコンパイル・リンクして生成される hex ファイルでは基本的に書きこめずに失敗する。これは mbed 環境でコンパイルした場合、親切にも SoftDevice などをマージした状態で hex を作ってくれるから。しかし FOTA するのはアプリケーションの部分だけなので、余計な部分を取り除く必要がある。
これは nRF51_OTA_strip.py を使えばできる。単に引数に入力と出力を与えればアプリケーション部分だけの hex ファイルを吐いてくれる。
ここを変えれば FOTA 版が落とせるみたいです。気付かなかった。ここで FOTA 版を選択すると、DFUService は自動的に組込まれてコンパイルされます。
できた hex ファイルをなんとかして Android か iPhone に転送する。Google Drive に突っ込むのがてっとり早い。
そして nRF Toolbox を使って DFU をする。このとき、Init packet がどうたらというダイアログがでるが No を選択する。
Device を選択して Upload をタップすれば DFU がはじまる。結構時間がかかる。
https://github.com/ARMmbed/nrf5x-dfu-bootloader
たぶんこれがそれっぽい。ビルドしてないので確認はしてない。
公式ツールは Android / iPhone だけなので、できない。
サードパーティで作ってる人がいる。https://github.com/jeremysf/nrfDFU が、手元だとうまく動かすことができなかった。追試が必要。
ちっちゃな変更を3つほど送った。
なんか pcb2gcode を実行したら刺さるので、こまったなあと思ったら設定ミスがわかった。pcb2gcode 側でせめてwarningぐらい出せや、と思ってかっとなって書いたプルリク。
メモ書き:KiCAD + pcb2gcode で pcbmilling | tech - 氾濫原 のとき書いたパッチ。pcb2gcode は --milldrill オプションをつけるとエンドミルを使ってすべての穴をあけることができる。たとえばφ0.8mm 以上の穴しかないなら、φ0.8mmのエンドミルで全ての穴をあけることができる。
ただ、このとき使われるオプションが --cutter-diameter だった。このオプションは外形カット時に使われるオプションなので、外形カットとドリルのときとでエンドミル径を変えることができなかった。
このパッチで --milldrill-diameter として穴をあけるときのエンドミル径を上書きできるようになった。
grbl だと G2 に X/Y がない場合、たんに無視されるという挙動をして絶望した。実際に機械を動かして穴をあけてから「あれ? ちゃんと開いてないぞ?」と気付いたので、目の前には加工途中の基板があった。原点がずれるとやっかいなので後日にすることもできず、勘でパッチを書いたら動いてくれた。grbl のコードも pcb2gcode のコードも手元にクローンしていてよかった……
ここまで既にマージ済み。めんどくさいコントリビューションルールもなく、非常にレスポンスはやくレビューしてくれて、良かった。
あとこうやってプルリク送った経緯と書いておくのは良い気がするので送ったときには書いていきたい。
BLE Nano をあいかわらず触っている。どうしても消費電流の削減ができず3日ぐらい悩んだので、参考までに「どうすれば効率よく消費電流を削減できそうか」をしるす。
nRF51822_PS v3.1.pdf と nRF51_Series_Reference_manual v3.0.pdf というのが主要なドキュメントになる。前者には nRF51822 特有のことがら全般が書かれていて、こちらに消費電力や、ピンごとの物理仕様が書いてある。後者は nRF51 シリーズのシステムのドキュメントになっており、レジスタ仕様とかが書いてある。
消費電力の観点で考えると、まず nRF51822_PS v3.1.pdf に一通り目を通して、どの回路がどのぐらいの電力消費をするかを把握しておくと良い。
当然支配的なところから解決しないとどうしよもないので、大きいところをとりあえずおさえる。具体的には
タイトルの 1mA という数字は HFCLK の消費のことを想定している。このクロックは必要ならば動くという挙動をする。CPU が動いているなら必ず動いている。
大事なのは以下の表 (nRF51822_PS v3.1.pdf から引用)
ここで HFCLK に依存しているブロックがスリープ中に一切ないようにしなければならない。nRF51_Series_Reference_manual v3.0.pdf のほうにはどのペリフェラルが HFCLK に依存しているかは書いてないので、この表はとても大事。
だいたいのブロックは必要なときだけ有効にして動かす系だけど、UART や TWI のようにだいたいオンにしてるみたいなペリフェラルもスリープ前に明示的にオフにする必要がある。というよりは必要なときだけオンにするという使いかたのほうが安全。
TWI なら
NRF_TWI0->ENABLE = TWI_ENABLE_ENABLE_Enabled << TWI_ENABLE_ENABLE_Pos; do_something(); NRF_TWI0->ENABLE = TWI_ENABLE_ENABLE_Disabled << TWI_ENABLE_ENABLE_Pos;
UART なら
NRF_UART0->ENABLE = (UART_ENABLE_ENABLE_Enabled << UART_ENABLE_ENABLE_Pos); NRF_UART0->TASKS_STARTTX = 1; NRF_UART0->TASKS_STARTRX = 1; // dummy send to wakeup... NRF_UART0->PSELTXD = 0xFFFFFFFF; NRF_UART0->EVENTS_TXDRDY = 0; NRF_UART0->TXD = 0; while (NRF_UART0->EVENTS_TXDRDY != 1); NRF_UART0->PSELTXD = tx; do_something(); while (NRF_UART0->EVENTS_TXDRDY != 1); uint32_t tx = NRF_UART0->PSELTXD; NRF_UART0->TASKS_STOPTX = 1; NRF_UART0->TASKS_STOPRX = 1; NRF_UART0->ENABLE = (UART_ENABLE_ENABLE_Disabled << UART_ENABLE_ENABLE_Pos);
みたいな感じになる。UART はなんかバグってるのかよくわからないけど、ダミーで一回書かないとちゃんと復帰しなくてこまった。mbed のライブラリにも同様のことが書いてある。
書きこみインターフェイス (デバッガ) で書きこんだ直後のプログラムはデバッグモードで動いていて、この状態だと上記と同じように 16MHz とデバッガ用の回路が動くようで、消費電力がまったく減らない。
この状態から抜けてノーマルモードで起動するには
あたりがある。前者は若干めんどうなので、後者の方法のほうがおすすめ。
// Enable Pin-reset on DEBUG mode NRF_POWER->RESET = 1;
と main の冒頭あたりに書いておくと、デバッグモード中でもピンリセットがかけられる。「ピンリセットってどのピン?」と思うかもしれないが、SWDIO が nRESET と共用になっているので、書きこみ気から SWDIO/SWDCLK を抜いて GND に一瞬つなげばノーマルモードで起動するようになる。
BLE Nano は P0_19/D13 に LED がついてる。この LED は VDD に繋っており、負論理で光る。なので、この LED を消したいときは明示的に PullUp するか出力に設定して HIGH にする必要がある。
DigitalIn unused_p0_19(P0_19, PullUp);
ちなみに英語で検索するときは nRF51 power consumption とかでググるのが良いです。
メーデー!9:航空機事故の真実と真相 (吹替版) cho45
プライムビデオでシーズン11、10と9の一部が見れるのでずっと見てる。
航空業界は事故調査がものすごく発達しているというのを感じることができる。機械的な要因からパイロットの心理的な要因まで、とことん分析される。淡々とすすむので、見ていて疲れないが、好奇心は刺激されるので見ていてメリットしかない。
なぜ航空業界にはここまで厳しい事故調査が行われるのに、他の業界ではちゃんとしたものがないのだろうと考える。一発で大量に人が死ぬってのがインパクトがあることだから、というのがあるのだろうなと思った。自動車よりも飛行機のほうが圧倒的に安全なのにも関わらず、人間は飛行機を過剰に怖がる。
ブログラムのバグも、本来なら個人の力量に帰着せず、なぜそれをフレームワークや職場環境でカバーできなかったのかとかを深く考えるべきものであるはずだけど、基本的にプログラムのバグ1個で直接物理的に人が大量に死んだりはあまりないので、そういうところまで深く考える人が少ない。つまりプログラムで大量に人が殺せればプログラムのバグは適切に対処されるようになるだろう。人間が死なないと人間は学ばない。
Androud N がリリースされたようですが、ZenFone 2 には Android M が一向にきません。こないんでしょうか。
自宅のネットワーク、ちょいちょい特定のDNSがひけなくなるけど、なんなんだろうなあ。プロバイダのキャッシュサーバーがおかしいのだろうか……
『この美術には問題がある!』と『NEW GAME』が安心してみれて無限に可愛い女の子が喋る感じなのが良い
とりあえずメモだけ
Eagle の場合は pcb-gcode を使えばよかったが、これは Eagle の ULP で書かれているので KiCAD には使えない。pcb2gcode はガーバーファイルから直接 gcode に変換するのでどちらでも使える。
以下のパッチのブランチで実行
https://github.com/pcb2gcode/pcb2gcode/pull/47
millproject
# Use standard mm metric=true metricoutput=1 # front= back=Main-B.Cu.gbr outline=Main-Edge.Cuts.gbr drill=Main.drl back-output=back.gcode outline-output=outline.gcode drill-output=drill.gcode # https://github.com/chrysn-pull-requests/pcb2gcode/blob/graphical-documentation/man/options.svg zwork=-0.1 zsafe=1 mill-feed=500 mill-speed=10000 mill-vertfeed=200 offset=0.07679491924311227 #offset=10 extra-passes=1 zdrill=0.9 zchange=20 drill-feed=100 drill-speed=10000 milldrill=true # milldrill のときの直径 パッチが必要 milldrill-diameter=0.8 # for grbl nog81=true # 外形カット時のミル直径 cutter-diameter=1 zcut=-1 cut-feed=500 cut-speed=10000 cut-infeed=1 cut-side=back optimise=true zero-start=true dpi=1000
offset には削るエンドミルの半径を入れる。たとえば 30° 0.1mm のVカッターを -0.1mm で掘る (zwork=-0.1) にする場合 φ0.153mm なので、offset=0.0767 にする。
各 gcode を --zero-start で別々に生成すると原点がずれる。しかも --cutter-diameter を変えると外形レイヤーの基準がかわってしまう。
milldrill の場合 drill 系のオプションは無意味で、全て cutter 系のオプションが適用される。
milldrill のときの mill の直径は cutter-diameter と共用のようで、外形カットと穴開けで別の直径のエンドミルにできない…
KiCAD の時点で外形を大きくしておくか、パッチ書くしかない。→ 書いた
pcb2gcode は isolate な部分を全て削る設定にすることができない(と思う)。なので孤立した銅箔が生成されてしまう。 extra-passes=100 とかにすればできるだけ全て削れるっぽい。
offset を大きく設定すると voronoi アルゴリズムが有効になり、孤立した部分をなくせる (孤立した部分は近くの配線に吸収される)。配線というよりは銅箔面の領域分割という感じになる。PCB っぽくはないけどおもしろい。
まだこれは試してない。
http://www.apache.org/dist/httpcomponents/httpcore/RELEASE_NOTES-4.4.x.txt
httpcore-4.4.5 (2016-06-08) では 4.4.4 ではあったアノテーションが削られています。
Please note the following annotations originally based on CC-BY licensed source have been removed in this release: org.apache.http.annotation.GuardedBy org.apache.http.annotation.Immutable org.apache.http.annotation.NotThreadSafe org.apache.http.annotation.ThreadSafe
httpclient 4.5.1 や 4.5.2 はこれらのアノテーションを使ってるので、うっかり 4.4.5 が入ると死ぬようです。
以下のようにバージョン指定しました。
compile('org.apache.httpcomponents:httpcore:4.4.4')
compile('org.apache.httpcomponents:httpclient:4.5.2') 1時間ぐらいハマった。Java は辛い。
スマートメータのBルートサービスで Wi-SUN モジュールを使って瞬間消費電力を読み出す | tech - 氾濫原 にひき続き Wi-SUN モジュール ROHM BP35A1 と ECHONET Lite プロトコルを使い、スマートメータから値を取得するサンプルです。
前回のコードはさすがにちゃんと動かなすぎるものなので、多少まともにしたものを書きました。一応16時間ぐらい動かしても止まることなく動く感じです。
連続して動かす場合大事なところ
途中に環境変数で分岐していますが、片方はテスト用のコードです。Wi-SUN のスキャンが結構時間がかかってイライラするので、想定問答をシミュレーションしています。ちゃんとテスト化したほうがいいんですが、長時間実際に動かしてみるほうが有益だと思ったので限られた時間でそこまでやってません。
#!/usr/bin/env ruby -v
require 'stringio'
module ECHONET_Lite
EHD1 = 0b00010000
EHD2_DEFINED = 0x81
EHD2_ANY = 0x82
class ParseError < Exception
end
def self.parse_frame(frame)
ret = Frame.parse(frame)
unless ret.valid?
raise ParseError.new("not an ECHONET Lite frame")
end
ret
end
Frame = Struct.new(:ehd1, :ehd2, :tid, :edata) do
def self.parse(frame)
ret = self.new(*frame.unpack("CCna*"))
if ret.valid? && ret.format_defined?
ret.edata = EDATA.parse(ret.edata)
end
ret
end
def valid?
ehd1 == EHD1
end
def format_defined?
ehd2 == EHD2_DEFINED
end
def format_any?
ehd2 == EHD2_ANY
end
def pack
[ehd1, ehd2, tid].pack("CCn") + edata.pack
end
end
EDATA = Struct.new(:seoj, :deoj, :esv, :opc, :properties) do
def self.parse(edata)
ret = self.new(*edata.unpack("a3a3CCa*"))
ret.seoj = EOJ.parse(ret.seoj)
ret.deoj = EOJ.parse(ret.deoj)
props = []
StringIO.open(ret.properties) do |io|
ret.opc.times do
epc, pdc = *io.read(2).unpack("CC")
edt = io.read(pdc)
props << Property.new(epc, pdc, edt)
end
end
ret.properties = props
ret
end
def pack
seoj.pack + deoj.pack + [esv, opc].pack("CC") + properties.map {|i|
i.pack
}.join
end
end
EOJ = Struct.new(:class_group_code, :class_code, :instance_code) do
def self.parse(eoj)
self.new(*eoj.unpack("CCC"))
end
def pack
to_a.pack("CCC")
end
end
Property = Struct.new(:epc, :pdc, :edt) do
def pack
self.pdc = edt.length
[epc, pdc].pack("CC") + edt
end
end
end
require 'thread'
class SKSTACK_IP
EVENT_RECV_NS = 1
EVENT_RECV_NA = 2
EVENT_RECV_ECHO = 5
EVENT_COMPLETED_ED_SCAN = 0x1F
EVENT_RECV_BEACON = 0x20
EVENT_UDP_SENT = 0x21
EVENT_COMPLETED_ACTIVE_SCAN = 0x22
EVENT_PANA_ERROR = 0x24
EVENT_PANA_COMPLETED = 0x25
EVENT_RECV_SESSION_CLOSE = 0x26
EVENT_PANA_CLOSED = 0x27
EVENT_PANA_TIMEOUT = 0x28
EVENT_SESSION_EXPIRED = 0x29
EVENT_SEND_LIMIT = 0x32
EVENT_SEND_UNLOCK = 0x33
def initialize(port)
@event_callbacks = {}
@port = port
@port.set_encoding(Encoding::BINARY)
@rest = nil
@queue = Queue.new
@read_thread = Thread.start do
Thread.current.abort_on_exception = true
buffer = ""
while true
# need to know command name preceded by whole line
# because there is ERXUDP/ERXTCP which include length and any binary bytes.
c = @port.getc
if c.nil?
raise "unexpected IO closed"
end
buffer << c
case c
when ' ', "\r"
command = buffer.sub(/[\r ]$/, '')
case command
when "ERXUDP"
event = {}
event[:sender] = @port.gets(" ").sub(/\s+$/, '')
event[:dest] = @port.gets(" ").sub(/\s+$/, '')
event[:rport] = @port.gets(" ").sub(/\s+$/, '').unpack("n")[0]
event[:lport] = @port.gets(" ").sub(/\s+$/, '').unpack("n")[0]
event[:senderlla] = @port.gets(" ").sub(/\s+$/, '')
event[:secured] = @port.gets(" ").sub(/\s+$/, '')
datalen = @port.gets(" ").sub(/\s+$/, '')
event[:data] = @port.read(datalen.to_i(16))
@port.read(2) # ignore crlf
callback_event(:ERXUDP, event)
buffer.clear
when "ERXTCP"
event = {}
event[:sender] = @port.gets(" ").sub(/\s+$/, '')
event[:rport] = @port.gets(" ").sub(/\s+$/, '')
event[:lport] = @port.gets(" ").sub(/\s+$/, '')
datalen = @port.gets(" ").sub(/\s+$/, '')
event[:data] = @port.read(datalen.to_i(16))
@port.read(2) # ignore crlf
callback_event(:ERXTCP, event)
buffer.clear
when "EPONG"
event = {}
event[:sender] = @port.gets("\n").sub(/\s+$/, '')
callback_event(:EPONG, event)
buffer.clear
when "ETCP"
event = {}
event[:status] = @port.gets(" ").sub(/\s+$/, '')
if event[:status] == "1"
event[:handle] = @port.gets(" ").sub(/\s+$/, '')
event[:ipaddr] = @port.gets(" ").sub(/\s+$/, '')
event[:rport] = @port.gets(" ").sub(/\s+$/, '')
event[:lport] = @port.gets("\n").sub(/\s+$/, '')
else
event[:handle] = @port.gets("\n").sub(/\s+$/, '')
end
callback_event(:EPONG, event)
buffer.clear
when "EADDR", "ENEIGHBOR"
# ignore
when "EPANDESC"
event = {}
@port.gets("\n") # ignore
event[:channel] = @port.gets("\n")[/Channel:(\S+)/, 1]
event[:channel_page] = @port.gets("\n")[/Channel Page:(\S+)/, 1]
event[:pan_id] = @port.gets("\n")[/Pan ID:(\S+)/, 1]
event[:addr] = @port.gets("\n")[/Addr:(\S+)/, 1]
event[:lqi] = @port.gets("\n")[/LQI:(\S+)/, 1]
event[:pair_id] = @port.gets("\n")[/PairID:(\S+)/, 1]
p event
callback_event(:EPANDESC, event)
buffer.clear
when "EEDSCAN"
@port.gets("\n") # ignore
_rssi = @port.gets("\n")
when "EPORT"
@port.gets("\n") # ignore
6.times do
_udp = @port.gets("\n") # ignore
end
@port.gets("\n") # ignore
4.times do
_tcp = @port.gets("\n") # ignore
end
@port.gets("\n") # "OK" ignore
when "EHANDLE"
@port.gets("\n") # ignore
while line = @port.gets("\n")
line.chomp!
break if line == "OK"
end
when "EVENT"
num, sender, param = *@port.gets("\n").sub(/\s+$/, '').split(/ /)
event = {
num: num,
sender: sender,
param: param
}
callback_event(:EVENT, event)
buffer.clear
when "EVER"
event = {}
event[:version] = @port.gets("\n").sub(/\s+$/, '')
callback_event(:EVER, event)
buffer.clear
when "EAPPVER"
event = {}
event[:version] = @port.gets("\n").sub(/\s+$/, '')
callback_event(:EAPPVER, event)
buffer.clear
else
# do nothing
end
when "\n"
# event 以外
line = buffer.chomp
@queue << line
buffer.clear
end
end
end
end
def command(string)
@port.write(string + "\r\n")
res = @queue.pop
if string.split(/ /)[0] == res.split(/ /)[0] # ignore echoback
res = @queue.pop
end
res
end
def on(name, &block)
(@event_callbacks[name.to_sym] ||= []) << block
end
private
def callback_event(name, event)
(@event_callbacks[name.to_sym] || []).each do |cb|
cb.call(event)
end
end
end
require 'logger'
require 'timeout'
class SmartMeterController
def initialize
@logger = Logger.new($stdout)
end
def start(io, opts)
@stack = SKSTACK_IP.new(io)
@events = Queue.new
@stack.on(:EVENT) do |e|
@logger.debug("EVENT %p" % e)
@events << e
end
@epandesc = nil
@stack.on(:EPANDESC) do |e|
@logger.debug("EPANDESC %p" % e)
@epandesc = e
end
@transactions = {}
@stack.on(:ERXUDP) do |e|
@logger.info("ERXUDP %p" % e)
begin
frame = ECHONET_Lite.parse_frame(e[:data])
if transaction = @transactions.delete(frame.tid)
transaction.call(frame)
end
rescue ECHONET_Lite::ParseError
@logger.info("Not an ECHONET Lite frame")
end
end
@stack.on(:EVER) do |e|
@logger.info("EVER %p" % e)
end
@stack.on(:EAPPVER) do |e|
@logger.info("EAPPVER %p" % e)
end
@stack.command("SKRESET") == "OK" or raise
@stack.command("SKVER") == "OK" or raise
@stack.command("SKAPPVER") == "OK" or raise
@stack.command("SKSREG SFE 0") == "OK" or raise
@logger.info("Setting ID and Password")
@stack.command("SKSETPWD C #{opts[:PASS]}") == "OK" or raise
@stack.command("SKSETRBID #{opts[:ID]}") == "OK" or raise
while true
@logger.info("Scanning device...")
@stack.command("SKSCAN 2 FFFFFFFF 6")
while e = @events.pop
if e[:num].to_i(16) == SKSTACK_IP::EVENT_COMPLETED_ACTIVE_SCAN
@logger.info("Scan Completed")
break
end
end
if @epandesc
break
end
@logger.info("Device not found... retrying...")
sleep 1
end
@logger.info("Device found %p" % @epandesc)
@logger.info("Getting IPv6 Address from MAC Address (%p)" % @epandesc[:addr])
@ipv6_addr = @stack.command("SKLL64 #{@epandesc[:addr]}")
@logger.info("Setting Channel and Pan ID")
@stack.command("SKSREG S2 #{@epandesc[:channel]}") == "OK" or raise
@stack.command("SKSREG S3 #{@epandesc[:pan_id]}") == "OK" or raise
@logger.info("Starting PANA")
@stack.command("SKJOIN #{@ipv6_addr}") == "OK" or raise
while e = @events.pop
case e[:num].to_i(16)
when SKSTACK_IP::EVENT_PANA_COMPLETED
break
when SKSTACK_IP::EVENT_PANA_ERROR
raise "pana error"
end
end
@logger.info("PANA Completed")
@tid = 0
end
def retrieve_power
@tid += 1
tid = @tid
q = Queue.new
@transactions[tid] = proc {|frame|
q << frame
}
frame = ECHONET_Lite::Frame.new(
ECHONET_Lite::EHD1,
ECHONET_Lite::EHD2_DEFINED,
@tid,
ECHONET_Lite::EDATA.new(
ECHONET_Lite::EOJ.new(0x05, 0xFF, 0x01),
ECHONET_Lite::EOJ.new(0x02, 0x88, 0x01),
0x62,
1,
[
ECHONET_Lite::Property.new(
0xe7,
0x00,
""
)
]
)
)
handle = 1
port_num = 3610
sec = 1
data = frame.pack
p [:packed, data]
@stack.command("SKSENDTO %s %s %04X %s %04X %s" % [
handle,
@ipv6_addr,
port_num,
sec,
data.length,
data
])
while e = @events.pop
if e[:num].to_i(16) == SKSTACK_IP::EVENT_UDP_SENT
unless e[:param].to_i(16) == 0 # success
return nil
end
break
end
end
ret = nil
begin
Timeout.timeout(5) do
ret = q.pop
end
rescue Timeout::Error
@logger.info "UDP Response Timeout"
@transactions.delete(tid)
end
ret
end
end
io = nil
if ENV["PORT"].nil?
require 'socket'
s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
Thread.start do
while l = s2.gets
l.chomp!
case l
when "SKVER"
s2 << "SKVER\r\n"
s2 << "EVER 1.2.10\r\n"
s2 << "OK\r\n"
when "SKSREG SFE 0"
s2 << "SKSREG SFE 0\r\n"
s2 << "OK\r\n"
when /^SKSETPWD/
s2 << "OK\r\n"
when /^SKSETRBID/
s2 << "OK\r\n"
when "SKSCAN 2 FFFFFFFF 6"
s2 << "OK\r\n"
s2 << "EVENT 20 FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX\r\n"
s2 << "EPANDESC\r\n"
s2 << " Channel:2F\r\n"
s2 << " Channel Page:09\r\n"
s2 << " Pan ID:A0E6\r\n"
s2 << " Addr:001C64000357XXXX\r\n"
s2 << " LQI:84\r\n"
s2 << " PairID:00AXXXXX\r\n"
s2 << "EVENT 22 FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX\r\n"
when /^SKLL64/
s2 << "FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY\r\n"
when /^SKSREG S2/
s2 << "OK\r\n"
when /^SKSREG S3/
s2 << "OK\r\n"
when /^SKJOIN/
s2 << "OK\r\n"
s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 02\r\n"
s2 << "EVENT 02 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY\r\n"
s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0028 (����O�y\r\n"
s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0068 h����O�z$�r%^a�;H)��#L�8�8/�4+�u\-����&ѨSM00000099021000000000000000AXXXXX\r\n"
s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0054 T����O�{;�;/��4+�u\-����&Ѩ��?s�����r��2���0��D�R��\r\n"
s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0058 X����O�|� Q�{��(F���.�[�<\r\n"
s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
s2 << "EVENT 25 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY\r\n"
when /SKSENDTO/
s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 0E1A 0E1A 001C64000357XXXX 1 0012 \x10\x81\x00\x01\x02\x88\x01\x05\xFF\x01r\x01\xE7\x04\x00\x00\x04Z\r\n"
end
end
end
io = s1
else
require 'serialport'
begin
io = SerialPort.new(
"/dev/tty.usbserial-A500YQPG",
115200,
8,
1,
0
)
rescue Errno::EBUSY
sleep 1
retry
end
end
c = SmartMeterController.new
c.start(io, {
ID: "0000 00XX 0XXX 0000 0000 0000 XXXX XXXX".gsub(/ /, ''),
PASS: "XXXX XXXX XXXX".gsub(/ /, ''),
})
loop do
frame = c.retrieve_power
unless frame
puts "failed to get power"
next
end
frame.edata.properties.each do |prop|
p prop
if prop.epc == 0xe7 && prop.pdc == 4
watts = prop.edt.unpack("N")[0]
p "#{watts} W"
end
end
end