BLE Nano は書きこみ器セットで購入しても $32.90 とかなりお得。BLE Nano Kit - Product 単体 なら $17.90。RedBear は香港みたい。公式の通販から最低送料のオプションで買っても割とすぐ届く。

ただのビーコンとして使うには高価に感じるかもしれないが、PCとの低消費電力無線通信デバイスでこの価格帯のものは殆どない。

そしてARM かつ、mbed を開発環境につかえる。なお Arduino からも書きこめる。ナイス。

載っている BLE モジュールも nRF51822 という界隈でデファクトスタンダートみたいなやつなので比較的情報が豊富。

そして小さい。小さいのは正義。その分IOは少ないが割となんとかなる。

  1. トップ
  2. tech
  3. なぜ BLE Nano にご執心なのか

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 を開きっぱなしにしておけば、接続情報について常にデバッグログに残る。

  1. トップ
  2. tech
  3. BLE Nano (nRF51) + mbed でセキュリティ付きペアリングをして 0x3d エラーがでる

仕様に書いてあるが標準だと 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 レジスタは変更済みなので、必要ないところは上書きしないように注意する必要がある。

  1. トップ
  2. tech
  3. BLE Nano (nRF51822) のドライブ能力を外付け部品なしで拡張する

nRF51 での FOTA の仕組み

DFUService というのが mbed の BLE_API だと提供されていて勘違いしたけど、これは実際のファームウェア書きこみ処理は一切行わない。これがやっていることは bootloader を起動するということだけだ。

FOTA の仕組みとしては

  1. クライアントは DFUService に対してリクエスト
  2. DFUService はアプリケーション抜けて bootloader として再起動する
  3. クライアントは再度 DFU を見つけて通信を行う
  4. bootloader はBLE経由でデータを受けとってFlashに書きこむ
  5. bootloader はアプリケーションを起動する

という感じですすむ。

mbed 環境でのやりかた

まず、コンパイル済みの bootloader が必要で、これを USB 経由で書きこむ。これで準備完了になるので、これ以降は FOTA だけで書く必要がある。USB 経由で書きこむと bootloader を上書きしてしまうので、FOTA は無効になる 。

bootloader はどれを使うか

https://github.com/RedBearLab/nRF51822-Arduino/tree/S130/bootloader

Arduino IDE 経由で書きこめる bootloader になっているが、FOTA の機能もついている。BLE Nano だとこれ使っておけば良さそう。他のも使ってみたがこれだけ動いた。

mbed からデフォルトでダウンロードされる hex は書けない

ただ、上記 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 がはじまる。結構時間がかかる。

bootloader のソースコードは?

https://github.com/ARMmbed/nrf5x-dfu-bootloader

たぶんこれがそれっぽい。ビルドしてないので確認はしてない。

OS X で DFU できないの?

公式ツールは Android / iPhone だけなので、できない。

サードパーティで作ってる人がいる。https://github.com/jeremysf/nrfDFU が、手元だとうまく動かすことができなかった。追試が必要。

  1. トップ
  2. tech
  3. mbed + BLE Nano で FOTA (DFUService) を使うには?

ちっちゃな変更を3つほど送った。

add error for positive --zcut by cho45 · Pull Request #46 · pcb2gcode/pcb2gcode · GitHub

なんか pcb2gcode を実行したら刺さるので、こまったなあと思ったら設定ミスがわかった。pcb2gcode 側でせめてwarningぐらい出せや、と思ってかっとなって書いたプルリク。

Milldrill diameter (--milldrill-diameter option) by cho45 · Pull Request #47 · pcb2gcode/pcb2gcode · GitHub

メモ書き:KiCAD + pcb2gcode で pcbmilling | tech - 氾濫原 のとき書いたパッチ。pcb2gcode は --milldrill オプションをつけるとエンドミルを使ってすべての穴をあけることができる。たとえばφ0.8mm 以上の穴しかないなら、φ0.8mmのエンドミルで全ての穴をあけることができる。

ただ、このとき使われるオプションが --cutter-diameter だった。このオプションは外形カット時に使われるオプションなので、外形カットとドリルのときとでエンドミル径を変えることができなかった。

このパッチで --milldrill-diameter として穴をあけるときのエンドミル径を上書きできるようになった。

Clearly specify X/Y to G2. by cho45 · Pull Request #48 · pcb2gcode/pcb2gcode · GitHub

grbl だと G2 に X/Y がない場合、たんに無視されるという挙動をして絶望した。実際に機械を動かして穴をあけてから「あれ? ちゃんと開いてないぞ?」と気付いたので、目の前には加工途中の基板があった。原点がずれるとやっかいなので後日にすることもできず、勘でパッチを書いたら動いてくれた。grbl のコードも pcb2gcode のコードも手元にクローンしていてよかった……


ここまで既にマージ済み。めんどくさいコントリビューションルールもなく、非常にレスポンスはやくレビューしてくれて、良かった。

あとこうやってプルリク送った経緯と書いておくのは良い気がするので送ったときには書いていきたい。

  1. トップ
  2. tech
  3. pcb2gcode へのプルリク

BLE Nano をあいかわらず触っている。どうしても消費電流の削減ができず3日ぐらい悩んだので、参考までに「どうすれば効率よく消費電流を削減できそうか」をしるす。

ドキュメントを良くよむこと

nRF51822_PS v3.1.pdf と nRF51_Series_Reference_manual v3.0.pdf というのが主要なドキュメントになる。前者には nRF51822 特有のことがら全般が書かれていて、こちらに消費電力や、ピンごとの物理仕様が書いてある。後者は nRF51 シリーズのシステムのドキュメントになっており、レジスタ仕様とかが書いてある。

消費電力の観点で考えると、まず nRF51822_PS v3.1.pdf に一通り目を通して、どの回路がどのぐらいの電力消費をするかを把握しておくと良い。

当然支配的なところから解決しないとどうしよもないので、大きいところをとりあえずおさえる。具体的には

  • 16MHz HFCLK (クロックだけで約1mAぐらい食う)
  • CPU (起きているときは 4mA ぐらい食う)
  • Radio (送信出力ごとに最大電流が異なる 5.5〜16mA。かなり大きく見えるが BLE の場合送信にかける時間はかなり短いので、平均的には支配率はそれほど高くない)

スリープ中に 16MHz のクロックを切るためには

タイトルの 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 固有

BLE Nano は P0_19/D13 に LED がついてる。この LED は VDD に繋っており、負論理で光る。なので、この LED を消したいときは明示的に PullUp するか出力に設定して HIGH にする必要がある。

DigitalIn unused_p0_19(P0_19, PullUp);

備考

ちなみに英語で検索するときは nRF51 power consumption とかでググるのが良いです。

  1. トップ
  2. tech
  3. BLE Nano (nRF51822) でどうしても 1mA 以上電流食うぞというとき

メーデー!9:航空機事故の真実と真相 (吹替版) - ---

---

5.0 / 5.0

メーデー!10:航空機事故の真実と真相 (吹替版) - ---

---

5.0 / 5.0

メーデー!11:航空機事故の真実と真相 (吹替版) - ---

---

5.0 / 5.0

プライムビデオでシーズン11、10と9の一部が見れるのでずっと見てる。

航空業界は事故調査がものすごく発達しているというのを感じることができる。機械的な要因からパイロットの心理的な要因まで、とことん分析される。淡々とすすむので、見ていて疲れないが、好奇心は刺激されるので見ていてメリットしかない。

なぜ航空業界にはここまで厳しい事故調査が行われるのに、他の業界ではちゃんとしたものがないのだろうと考える。一発で大量に人が死ぬってのがインパクトがあることだから、というのがあるのだろうなと思った。自動車よりも飛行機のほうが圧倒的に安全なのにも関わらず、人間は飛行機を過剰に怖がる。


ブログラムのバグも、本来なら個人の力量に帰着せず、なぜそれをフレームワークや職場環境でカバーできなかったのかとかを深く考えるべきものであるはずだけど、基本的にプログラムのバグ1個で直接物理的に人が大量に死んだりはあまりないので、そういうところまで深く考える人が少ない。つまりプログラムで大量に人が殺せればプログラムのバグは適切に対処されるようになるだろう。人間が死なないと人間は学ばない。

 -

5.0 / 5.0

Huawei MediaPad T2 7.0 Pro を買ってみた。しばらくうちにはタブレットがなかったので、ひさびさにタブレットを手に入れた。

こんな価格だけど使用感は全く問題ない。指紋認証でロックが解除できて、思ったよりこれが便利。

PDF ドキュメント読むときに使うのがかなり快適だな〜 という感じ。Zenfone 2 と比べて2インチしか変わらないけど、ぜんぜん違う。

Androud N がリリースされたようですが、ZenFone 2 には Android M が一向にきません。こないんでしょうか。

自宅のネットワーク、ちょいちょい特定のDNSがひけなくなるけど、なんなんだろうなあ。プロバイダのキャッシュサーバーがおかしいのだろうか……

『この美術には問題がある!』と『NEW GAME』が安心してみれて無限に可愛い女の子が喋る感じなのが良い

とりあえずメモだけ

Eagle の場合は pcb-gcode を使えばよかったが、これは Eagle の ULP で書かれているので KiCAD には使えない。pcb2gcode はガーバーファイルから直接 gcode に変換するのでどちらでも使える。

レイアウト

KiCAD 書き出し


pcb2gcode

以下のパッチのブランチで実行

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 っぽくはないけどおもしろい。

まだこれは試してない。

  1. トップ
  2. tech
  3. メモ書き:KiCAD + pcb2gcode で pcbmilling

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 は辛い。

  1. トップ
  2. tech
  3. Error:java: cannot access org.apache.http.annotation.Immutable class file for org.apache.http.annotation.Immutable not found

スマートメータのBルートサービスで Wi-SUN モジュールを使って瞬間消費電力を読み出す | tech - 氾濫原 にひき続き Wi-SUN モジュール ROHM BP35A1 と ECHONET Lite プロトコルを使い、スマートメータから値を取得するサンプルです。

前回のコードはさすがにちゃんと動かなすぎるものなので、多少まともにしたものを書きました。一応16時間ぐらい動かしても止まることなく動く感じです。

連続して動かす場合大事なところ

  • UDP 送信時の失敗処理をちゃんとやること
  • タイムアウト処理をちゃんとやること
    • たとえこちらからの UDP の送信に成功しても、UDPパケットがこちらに必ず受信できる保証はない

途中に環境変数で分岐していますが、片方はテスト用のコードです。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
  1. トップ
  2. tech
  3. スマートメータから瞬間消費電力を読むRubyのコード

先週火曜日は体調不良で休んだが、子供も熱を出してひきとることになったので、結局完全には休めなかった。

鼻炎と喉がすこし痛いぐらいなので耳鼻科にいって薬をもらっているけど治らない。というか鼻炎と喉はともかく全身倦怠感がひどくてなにもできない。