アナログ回路への理解を深めたい。まだ全然、やりたいことをすぐに実現できるレベルにならない。どうすればできるのかわからない、ということが多すぎる。

2015年もたくさんコードかきました。他人に承認されないことはせめて自分で承認しましょう。つらい

Chemr

[tech] リファレンスマニュアルをインクリメンタル検索するやつを Electron で実装した | Wed, Oct 14. 2015 - 氾濫原

ドキュメントビューワ。Mac AppStore まで出してみたが全く売れておりません。自分では便利に使ってる

Arduino-meta

[tech] Arduino の digitalWrite / pinMode / digitalRead をコンパイル時に展開する | Wed, Dec 16. 2015 - 氾濫原

digitalWrite とかを静的に解決するやつ。

go-KX3-panadapter

[tech] もう僕らは OpenGL ライブラリにリンクするビルドに悩むことはない | Thu, Oct 8. 2015 - 氾濫原

KX3(無線機)用のパナダプター(Panoramic Adapter = FFTウォーターフォール) WebGL に書きなおしたりした。便利

GrblServer

[tech] CNC フライス Grbl 制御編 | Sun, Aug 16. 2015 - 氾濫原

CNCフライス制御用のアプリケーション。最近あんまり切削してない。

KeyCast

[tech] スクリーンキャスト用のキーストローク表示アプリ | Sat, Feb 14. 2015 - 氾濫原

入力したキーを画面に表示するやつ。

CopyHook

[tech] CopyHook というペーストボードの中身をいじるツールを作った | Wed, Mar 18. 2015 - 氾濫原

クリップボードコピーしたときにフックでJS実行するやつ。なんかうまく動いてない気がするがデバッグの時間がとれない。

reedsolomon.js

[tech] JavaScript で書かれたリードソロモン符号のエンコーダ・デコーダ | Tue, Mar 31. 2015 - 氾濫原

任意のリードソロモン(誤り訂正)符号のエンコーダデコーダ。Zxing (Java) の一部ライブラリの移植

webaudio-filter-frequency-response

[tech] WebAudio の BiquadFilterNode の周波数特性をグラフにするやつ | Fri, Mar 20. 2015 - 氾濫原

WebAudio のフィルタの周波数特性を出すやつ。

dekaimoji-a4

[tech] デカい文字をA4で分割して印刷するツールをJSで書いた | Sat, Mar 7. 2015 - 氾濫原

https://cho45.stfuawsc.com/dekaimoji-a4/

A4プリンタでデカい文字が印刷できるやつ。

  1. トップ
  2. tech
  3. 2015年つくったもの

ただのちっちゃい Linux だと思っていたけど、RTOS が入っているコプロセッサも持っていて、ホストCPUとシームレスに連携できるということがわかった。

アーキテクチャを見ると思いのほか面白かった。

https://software.intel.com/en-us/creating-applications-with-mcu-sdk-for-intel-edison-board

ホストCPU上のLinuxからは、GPIOが sysfs 経由 (または Intel の提供するSDKなど) から見える。それと同時にコプロセッサの MCU からは直接 GPIO が見える。

ホストCPUのLinuxはリアルタイムではないので、最低でも10ms程度(カーネルの割込み頻度の設定による)、処理が遅延する。GPIO を正確にコントロールする場合この遅延は大きすぎる。

一方コプロセッサのMCUはリアルタイムになっており、クロック100MHzなので、最速では 20ns ぐらいの単位でコントロールできる。

Edison はこれらをうまく協調して動かせるように設計されているみたい。MCU 側へファームウェアを書きこんで /dev/ttymcu* で連携したりとか、ホストCPUをスリープさせてMCUだけで動かしつつホストCPUをいい感じのタイミングで起こせるとか。

おもしろい!!

Intel ボードコンピューター Intel Edison Kit for Breakout Board(MM#939977) EDI2BB.AL.K - インテル

インテル

3.0 / 5.0

なお未だ買ってない。

  1. トップ
  2. tech
  3. Intel Edison のおもしろいところ

こんな感じで遊べる。

HttpClient (portformio だと id=66) の使いかたで微妙にハマった。

こんな感じの GrowthForecastClient クラスをつくっておいて、

HttpClient とホスト名や認証情報を渡して

WiFiClient wifiClient;
HttpClient http(wifiClient);
GrowthForecastClient gf(http, GF_HOST, GF_USER, GF_PASS);

https://github.com/cho45/esp-wroom-02-sketch/blob/master/src/main.cpp#L24

post する

	float temp = adt7410.read();
	Serial.print("adt7410 = ");
	Serial.println(temp);
	gf.post("/home/test/temp", (int32_t)(temp * 1000));

https://github.com/cho45/esp-wroom-02-sketch/blob/master/src/main.cpp#L165

GF は整数しか扱えないので、1000倍してpostして、GFのグラフの設定で ÷ 1000 を選択する。

HttpClient 以外はとくにハマりどころはない。適当にグラフ化するならこれ以上簡単なものはない。

ただ、結構 post に時間がかかる。特に request body にパラメータを書こうとするとあまりにも遅いのでクエリパラメータにしている。なんかどっかですごい効率が悪いことをしていそうだがよくわからない。

  1. トップ
  2. tech
  3. ESP8266 から GrowthForecast へセンサーデータをPOST

cmake で CGI (blosxom クローンぽいやつ) を書きました。(実用のためというわけではないです)

cmake は -P をつけるとスクリプトモードになるので、それでふつうにCGIのコードを書くだけです。

データ型として文字列しかないのがつらい感じです。リストみたいなのも使えるけど、これもセミコロン区切りの文字列をリストとみなすという仕様になっている。シェルスクリプトのノリに近い。

ただ、正規表現がある程度使えるので、文字列操作は割と楽に書くことができる。

なお、cmakeで普通使うような命令を使わない(使えない)のでまったく通常ユースのためのノウハウはほとんど溜まらず、学習という意味ではあんまり意味がないというオチでした。

  1. トップ
  2. tech
  3. cmake で CGI を書く

ESP8266 の deep sleep モードは CPU などの電源を全て落として、RTC (時刻を持ってるわけではないっぽいけど内蔵RTCがあるらしい) だけ生かして設定時刻になったら RTC にリセットさせるという挙動をする。

そういうわけで、ディープスリープ後はリセット直後と同じ状態になり、メモリなどは引き継げない。

もちろん deep sleep 時には wifi の接続も切れてしまうので、毎回繋ぎなおしになる。自分の環境だとAPに接続するまで5秒ぐらいかかるし、接続中は電流が80mA(データシート的には最大で180mA)ぐらい流れる。高い頻度で起動すると初期化のコストがかなり大きい。

ユースケース

センサーデータは数秒に一度取得したいが、データ送信はさらに少ない頻度でどこかに送信したい、というような場合。

system_rtc_mem_read / system_rtc_mem_write

ESP8266 自体にこういうAPIがあり、RTC にあるメモリ(揮発性)を読み書きすることができる。RTC は deep sleep でも有効なので、これは電源が接続されている限り保持されるメモリのようだ (電源を切ると初期化される)。ユーザ領域として最大512bytes使えることになっている。

ということで、これを使って起動回数を記録しつつ、何回かに一度 WiFi 接続するようなサンプルを書いてみた (実際接続はしてないけど)

未初期化の system_rtc_mem_read は不定値がかえってくるみたいなので、CRC なりなんなりでハッシュをとって明示的に初期化をしたほうがよさそう。

#include <Arduino.h>
extern "C" {
#include <user_interface.h>
};
	
// system_rtc_mem_write() 先のブロックアドレス。
// 4 bytes で align されており、先頭256bytes はシステム予約領域
// 64 から書けるはずだが、65 以降でないとうまくいかなかった。。
static const uint32_t USER_DATA_ADDR = 66;

// ハッシュ関数 (FNV) CRC でいいけどコード的に短いのでFNV
static uint32_t fnv_1_hash_32(uint8_t *bytes, size_t length) {
	static const uint32_t FNV_OFFSET_BASIS_32 = 2166136261U;
	static const uint32_t FNV_PRIME_32 = 16777619U;
	uint32_t hash = FNV_OFFSET_BASIS_32;;
	for(size_t i = 0 ; i < length ; ++i) hash = (FNV_PRIME_32 * hash) ^ (bytes[i]);
	return hash;
}

// struct の hash (先頭にあることを想定) を除くデータ部分のハッシュを計算する
template <class T>
uint32_t calc_hash(T& data) {
	return fnv_1_hash_32(((uint8_t*)&data) + sizeof(data.hash), sizeof(T) - sizeof(data.hash));
}

struct {
	// retain data
	uint32_t hash;
	uint16_t count;
	uint8_t  send;
	uint16_t etc2;
} retain_data;

void post_sensor_data();

void setup() {
	pinMode(13, OUTPUT);

	Serial.begin(9600);
	Serial.println("Initializing...");

	// データ読みこみ
	bool ok;
	ok = system_rtc_mem_read(USER_DATA_ADDR, &retain_data, sizeof(retain_data));
	if (!ok) {
		Serial.println("system_rtc_mem_read failed");
	}
	Serial.print("retain_data.count = ");
	Serial.println(retain_data.count);

	// ハッシュが一致していない場合、初期化されていないとみなし、初期化処理を行う
	uint32_t hash = calc_hash(retain_data);
	if (retain_data.hash != hash) {
		Serial.println("retain_data may be uninitialized");
		retain_data.count = 0;
		retain_data.send = 0;
	}

	// データの変更処理(任意)
	retain_data.count++;
	if (!retain_data.send) {
		// 4回に1度送信する
		retain_data.send = retain_data.count % 4 == 0;
	} else {
		Serial.println("send data");
		retain_data.send = 0;
		// なんか定期的に書きこみたい処理
		post_sensor_data();
	}

	// 書きこみ処理。hash を計算していれておく
	retain_data.hash = hash = calc_hash(retain_data);
	ok = system_rtc_mem_write(USER_DATA_ADDR, &retain_data, sizeof(retain_data));
	if (!ok) {
		Serial.println("system_rtc_mem_write failed");
	}

	// 動作確認用のダミー
	digitalWrite(13, HIGH);
	delay(1000);
	digitalWrite(13, LOW);

	if (retain_data.send) {
		ESP.deepSleep(1e6, WAKE_RF_DEFAULT);
	} else {
		// sendしない場合は WIFI をオフで起動させる
		ESP.deepSleep(1e6, WAKE_RF_DISABLED);
	}
}

void loop() {
}

// dummy
void post_sensor_data() {
	for (uint i = 0; i < 5; i++) {
		digitalWrite(13, HIGH);
		delay(300);
		digitalWrite(13, LOW);
		delay(300);
	}
}
  1. トップ
  2. tech
  3. ESP8266 (ESP-WROOM-02) の Deep Sleep でデータを引き継ぐ

ESP8266 (ESP-WROOM-02) の Deep Sleep でデータを引き継ぐ | tech - 氾濫原 を試しているとき、起動時に必ず一定のゴミっぽいのがつくので気になっていた。

が、実はこれブート時に必ずでるメッセージのようで、ボーレートがあっていない。ボーレートを 74880 にすると (書きこむコードでも 74880 を使うように変更)

と、完全なメッセージがみれる。ちなみに rst cause (リセット原因) は RTC メモリのシステム用の領域に入っているみたい。

そういうわけなので、デバッグ用に UART 使うなら 74880 にあわせておいたほうが気持ち悪くない。

  1. トップ
  2. tech
  3. ESP8266 起動時にシリアルにでるゴミっぽいもの

コード

以下のようなコードで試した。概要としては

  • 2.5秒に1度起動する (それ以外は deep sleep)
  • 起動時にデータをRTCメモリに書きむ (センサーデータ取得を想定)
  • 4回に1度 WiFi に接続し、UDP でセンサーデータを送信する

というもの

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include "rtc_memory.hpp"
#include "config.h"
WiFiClient wifiClient;
struct {
char SSID[255] = WIFI_SSID;
char Password[255] = WIFI_PASS;
} wifi_config;
bool startWifi(int timeout) {
WiFi.disconnect();
WiFi.mode(WIFI_STA);
Serial.println("Reading wifi_config");
Serial.println("wifi_config:");
Serial.print("SSID: ");
Serial.print(wifi_config.SSID);
Serial.print("\n");
if (strlen(wifi_config.SSID) == 0) {
Serial.println("SSID is not configured");
return false;
}
WiFi.begin(wifi_config.SSID, wifi_config.Password);
int time = 0;
for (;;) {
switch (WiFi.status()) {
case WL_CONNECTED:
Serial.println("connected!");
WiFi.printDiag(Serial);
Serial.print("IPAddress: ");
Serial.println(WiFi.localIP());
return true;
case WL_CONNECT_FAILED:
Serial.println("connect failed");
return false;
}
delay(500);
Serial.print(".");
time++;
if (time >= timeout) {
break;
}
}
return false;
}
struct deep_sleep_data_t {
uint16_t count = 0;
uint8_t send = 0;
uint16_t data[12];
void add_data(uint16_t n) {
data[count] = n;
}
template <class T>
void run_every_count(uint16_t n, T func) {
count++;
if (!send) {
send = count % (n - 1) == 0;
} else {
send = 0;
count = 0;
func();
}
}
};
rtc_memory<deep_sleep_data_t> deep_sleep_data;
void post_sensor_data();
void setup() {
pinMode(13, OUTPUT);
Serial.begin(74880);
Serial.println("Initializing...");
// データ読みこみ
if (!deep_sleep_data.read()) {
Serial.println("system_rtc_mem_read failed");
}
Serial.print("deep_sleep_data->count = ");
Serial.println(deep_sleep_data->count);
// データの変更処理(任意)
deep_sleep_data->add_data(deep_sleep_data->count);
deep_sleep_data->run_every_count(4, [&]{
Serial.println("send data");
// なんか定期的に書きこみたい処理
post_sensor_data();
});
if (!deep_sleep_data.write()) {
Serial.print("system_rtc_mem_write failed");
}
if (deep_sleep_data->send) {
ESP.deepSleep(2.5e6, WAKE_RF_DEFAULT);
} else {
// sendしない場合は WIFI をオフで起動させる
ESP.deepSleep(2.5e6, WAKE_RF_DISABLED);
}
}
void loop() {
}
// dummy
void post_sensor_data() {
if (startWifi(30)) {
// do http etc...
WiFiUDP udp;
IPAddress serverIP(192, 168, 0, 5);
udp.beginPacket(serverIP, 5432);
udp.write((uint8_t*)deep_sleep_data->data, sizeof(deep_sleep_data->data));
udp.endPacket();
// wait for sending packet
delay(1000);
} else {
Serial.println("failed to start wifi");
ESP.restart();
}
}
view raw main.cpp hosted with ❤ by GitHub
extern "C" {
#include <user_interface.h>
};
template <class T>
class rtc_memory {
// 4 bytes aligned memory block address in rtc memory.
// user data must use 64 or larger block.
// but system_rtc_mem_read() is failed for 64 so use 65.
static constexpr uint32_t USER_DATA_ADDR = 65;
static constexpr uint32_t USER_DATA_SIZE = 512 - ((USER_DATA_ADDR - 64) * 4);
static uint32_t fnv_1_hash_32(uint8_t *bytes, size_t length) {
static const uint32_t FNV_OFFSET_BASIS_32 = 2166136261U;
static const uint32_t FNV_PRIME_32 = 16777619U;
uint32_t hash = FNV_OFFSET_BASIS_32;
for (size_t i = 0 ; i < length ; ++i) hash = (FNV_PRIME_32 * hash) ^ (bytes[i]);
return hash;
}
uint32_t calc_hash(T& data) const {
return fnv_1_hash_32((uint8_t*)&data, sizeof(data));
}
public:
uint32_t hash;
T data;
static_assert(sizeof(T) <= (USER_DATA_SIZE - sizeof(hash)), "sizeof(T) it too big");
bool read() {
// Read memory to temporary variable to retain initial values in struct T.
rtc_memory<T> read;
// An initial rtc memory is random.
bool ok = system_rtc_mem_read(USER_DATA_ADDR, &read, sizeof(read));
if (ok) {
// Only hashes are matched and copy to struct.
if (read.hash == calc_hash(read.data)) {
memcpy(this, &read, sizeof(read));
}
}
return ok;
}
bool write() {
hash = calc_hash(data);
return system_rtc_mem_write(USER_DATA_ADDR, this, sizeof(*this));
}
T* operator->() {
return &data;
}
};
view raw rtc_memory.cpp hosted with ❤ by GitHub

実測

説明をいれると

約5秒でAPに接続できている。その後UDPで送信し、送信が終わるのをちょっと無駄に1秒待っている。

V: 31.72mA となっているのが画面内の平均だけど、WiFi 接続が2回起きているので、1サイクルの平均ではない。

1サイクル(約16秒)あたりは、WiFi に 80mA 6秒、スリープ 20uA 10秒とし平均30mA。

もし単三 NiMH 2400mAh 3本(公称1.2V * 3 = 3.6V)をLDOで3.3Vにして電源にしたとすると、バッテリ容量は2400mAh * 1.2V * 3 = 8640mWh。このケースのESP8266の平均消費電力は 3.6V * 30mA = 108mW (LDO損失こみ)。8640 / 108 = 80時間。

支配的なのは圧倒的にWiFi接続時間なので、とにかく周期を長くとれば、それだけ電池寿命は伸びる。実際のところ電池3本で実用的なのは5分以上かなあという気がする (5分間隔でも2ヶ月ぐらい)。

これよりも短い間隔でデータのやりとりをする場合、電池駆動は考えないほうがよさそう。どうしても電池駆動したい場合WiFi 使うのが間違っているので ZigBee とか使うべきなんだと思う。とはいえ ESP8266 が圧倒的に安すぎるので、なんでも WiFi でやりたくなってしまう。

  1. トップ
  2. tech
  3. メモ: ESP8266 での実測電流

経緯

メモ: ESP8266 での実測電流 | tech - 氾濫原 というのを書いたあと、WiFi のコネクションなしでもデータを適当に送信できたらいいのではないか?と考えました。

具体的には SSID などをビーコン情報としてコネクションレスでブロードキャストしているのだから、これにセンサーデータをつけてしまえばいいだろう、というアイデアです。

ということで ESP8266 の API を調べてみましたが、どうやら WiFi のフレームを生で扱うようなAPIはないようでした。じゃあSSID にデータを入れて softap としてビーコンを発射すればとも思いましたが、さすがに邪悪すぎます。

もうちょっとAPIを眺めていると ESP-NOW というのがあることに気付きました。独自拡張のような形でIEEE802.11のフレームを飛ばしてWiFiコネクションレスで送受信できるもののようです。これを使えば前述のような邪悪なことをしなくても同じようなことができそうです。

ESP-NOW の概念

常時データを受けとるスレーブと、必要なときにデータを送信するコントローラに分かれます (送受信はどちらもできる)。スレーブは常時チャンネルを受信していなければならないので、基本的に固定の電源が必要です。コントローラはデータを投げるだけなのでバッテリ駆動できます。

送受信するまえにお互いの Mac アドレスを知っている必要があります。ESP-NOW としては、お互いに一旦 softap となって BSSID (Macアドレス) を公開してペアリングすることを想定しているようです。

なお、(おそらく)フレームをそのまま送信しているだけなので、確実に到達する保証はなく、もし要求されるならACKみたいなものは自力で実装する必要もあります。

暗号化のサポートもあるようですがそこまでとりあえずは調べていません。

実装

スレーブとコントローラいずれも ESP8266 である必要があるので (ローレベルなIEEE802.11のフレームを扱えるなら互換デバイスは作れそうですが)、ESP8266 は2台必要です。

ペアリング部分はめんどうなので、肝心のところだけ試しに実装して試してみました。

スレーブ

スレーブはデータを受けとって表示するだけです。現実的なアプリケーションなら、STATION+AP モードにして、別のホストに中継するように動作させると思いますが、今回はAPモードで動作させています。

esp_now_register_send_cb してますが send してないので呼ばれません。

ピアのMacアドレスは、STATION_IF のMacアドレスです。

#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
	#include <espnow.h>
	#include <user_interface.h>
}

#define WIFI_DEFAULT_CHANNEL 1

uint8_t mac[] = {0x5C,0xCF,0x7F,0x8,0x37,0xC7};

void printMacAddress(uint8_t* macaddr) {
	Serial.print("{");
	for (int i = 0; i < 6; i++) {
		Serial.print("0x");
		Serial.print(macaddr[i], HEX);
		if (i < 5) Serial.print(',');
	}
	Serial.println("}");
}

void setup() {
	pinMode(13, OUTPUT);

	Serial.begin(74880);
	Serial.println("Initializing...");

	WiFi.mode(WIFI_AP);
	WiFi.softAP("foobar", "12345678", 1, 0);

	uint8_t macaddr[6];
	wifi_get_macaddr(STATION_IF, macaddr);
	Serial.print("mac address (STATION_IF): ");
	printMacAddress(macaddr);

	wifi_get_macaddr(SOFTAP_IF, macaddr);
	Serial.print("mac address (SOFTAP_IF): ");
	printMacAddress(macaddr);

	if (esp_now_init() == 0) {
		Serial.println("init");
	} else {
		Serial.println("init failed");
		ESP.restart();
		return;
	}

	esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
	esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {
		Serial.println("recv_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("data: ");
		for (int i = 0; i < len; i++) {
			Serial.print(" 0x");
			Serial.print(data[i], HEX);
		}
		Serial.println("");
	});
	esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
		Serial.println("send_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("status = "); Serial.println(status);
	});

	int res = esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_CONTROLLER,(uint8_t)WIFI_DEFAULT_CHANNEL, NULL, 0);

//	esp_now_unregister_recv_cb();
//	esp_now_deinit();
}

void loop() {
}

コントローラ

スレーブとほぼ同じですが ROLE が変わっています。適当なバイト列を esp_now_send() しています。

ピアのMacアドレスは、SOFTAP_IF のMacアドレスです。

#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
	#include <espnow.h>
	#include <user_interface.h>
}

#define WIFI_DEFAULT_CHANNEL 1

uint8_t mac[] = {0x1A,0xFE,0x34,0xEE,0x84,0x88};

void printMacAddress(uint8_t* macaddr) {
	Serial.print("{");
	for (int i = 0; i < 6; i++) {
		Serial.print("0x");
		Serial.print(macaddr[i], HEX);
		if (i < 5) Serial.print(',');
	}
	Serial.println("}");
}

void setup() {
	pinMode(13, OUTPUT);

	Serial.begin(74880);
	Serial.println("Initializing...");
	WiFi.mode(WIFI_STA);

	uint8_t macaddr[6];
	wifi_get_macaddr(STATION_IF, macaddr);
	Serial.print("mac address (STATION_IF): ");
	printMacAddress(macaddr);

	wifi_get_macaddr(SOFTAP_IF, macaddr);
	Serial.print("mac address (SOFTAP_IF): ");
	printMacAddress(macaddr);

	if (esp_now_init()==0) {
		Serial.println("direct link  init ok");
	} else {
		Serial.println("dl init failed");
		ESP.restart();
		return;
	}

	esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
	esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {
		Serial.println("recv_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("data: ");
		for (int i = 0; i < len; i++) {
			Serial.print(data[i], HEX);
		}
		Serial.println("");
	});
	esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
		Serial.println("send_cb");

		Serial.print("mac address: ");
		printMacAddress(macaddr);

		Serial.print("status = "); Serial.println(status);
	});

	int res = esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_SLAVE,(uint8_t)WIFI_DEFAULT_CHANNEL, NULL, 0);

	uint8_t message[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08 };
	esp_now_send(mac, message, sizeof(message));
	ESP.deepSleep(2.5e6, WAKE_RF_DEFAULT);

//	esp_now_unregister_recv_cb();
//	esp_now_deinit();
}

void loop() {
}

動作と消費電力

こんな普通に簡単な実装を書いてみたら普通に動きました。

スレーブ側のシリアル出力

コントローラの電流値の推移

この実装だと 2.5秒ごとに毎回送信しているので割と平均消費も大きいです。しかしそれでも、前回の16秒ごとのWiFi接続の30mAの半分以下です。

コントローラの別実装

ということで、2.5秒おきに deep sleep から復帰して値を RTC メモリ書きつつ、4回に1度はWiFiで送信するようなサンプルも書いてみました。これは前回のWiFiコネクション+UDPの部分をESP-NOWを使うように置き換えたものになります。

エントリ冒頭の画像はこれの横軸を広げたものです。

平均で6.8mAぐらいです。

備考

この実装ではフレームの再送などはしていないので、データが欠落する可能性があります。とはいえ UDP の実装でも同じことがいえるので、これが原因だからダメということにはならないでしょう。

しかしこの実装だとMacアドレス以外一切何も確認していないので、もし悪意のある人が近くにいれば変なデータを送りこむのは容易です。

ESP8266 を使ってできる無線通信ではこれ以上の省電力化は難しそうな気がします。あとは送信出力を下げるぐらいしかなさそうです。

  1. トップ
  2. tech
  3. ESP8266 の低消費電力の限界をさぐる (ESP-NOWを使ってみる)

ebay で IIC/I2C/TWI 1602 Serial Blue Backlight LCD Display という名称で売られている、激安の16x2のLCDモジュール ($3.19) をためしに動かしてみました。

商品説明ページに一切インターフェイスについての記述はなく、I2Cアドレスから調べる必要があります。とはいえ 16x2 はだいたいみんな同じプロトコルなのと、見た目的に普通のパラレルなLCDをI2C変換しているだけなようなので、簡単に動くだろうと踏みました。

コード

https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library そのままでいけました。アドレスも一緒だったので想定している仕様が完全に一緒なのでしょう。

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

void setup() {
	Serial.begin(9600);

	pinMode(13, OUTPUT);

	Serial.println("Initializing...");

	Wire.begin();

	lcd.begin();
	lcd.backlight();
	lcd.setCursor(0, 0);
	lcd.print("Hello, World");
	lcd.setCursor(0, 1);
	lcd.print("TEST");
}

void loop() {
	delay(1000);
	lcd.setCursor(0, 1);
	lcd.print(millis() / 1000);
}

備考

買ったものは、一旦動かして記録にしておかないと、あとあと「あれを使おう」と思ったとき二度手間です。

なお、16x2 でバックライト付きなのは国内でも aitendo とかがかなり安く売っているので、あんまり ebay 経由で買う必要はありません。

  1. トップ
  2. tech
  3. Use IIC/I2C/TWI 1602 Serial Blue Backlight LCD Display

ruby serialport は、このあたりで設定できるボーレートを列挙していて、これら以外のボーレートを設定できないようです (unknown baud rate 例外)。

Linux だと ioctl 経由で設定すれば任意の設定ができるので、以下のようにして無理矢理設定します。(Raspberry Pi でテスト済み)

完全に任意ではなく、近いボーレートに丸められることがあるようです。

#!/usr/bin/env ruby

require "serialport"

SerialPort.class_eval do
	TCGETS2 = 0x802c542a
	TCSETS2 = 0x402c542b
	CBAUD   = 0x100f
	BOTHER  = 0x1000

	# struct termios2 {
	Termios2 = Struct.new(*[
		:c_iflag,
		:c_oflag,
		:c_cflag,
		:c_lflag,
		:c_line,
		(0...19).map {|n| # c_cc[NCCS] NCCS = 19
			"c_cc_#{n}".to_sym
		},
		:c_ispeed,
		:c_ospeed
	].flatten);
	Termios2::FORMAT = "I!I!I!I!CC19I!I!"
	Termios2::FORMAT_POINTER = "P44"
	# }

	def set_custom_baudrate(baud)
		tio = Termios2.new
		tio.each_pair {|m,_| tio[m] = 0 }

		# read
		v = tio.values.flatten.pack(Termios2::FORMAT)
		self.ioctl(TCGETS2, v)
		tio = Termios2.new(*v.unpack(Termios2::FORMAT))

		# write
		tio.c_cflag &= ~CBAUD
		tio.c_cflag |= BOTHER
		tio.c_ispeed = baud
		tio.c_ospeed = baud
		v = tio.values.flatten.pack(Termios2::FORMAT)
		self.ioctl(TCSETS2, v)

		# read
		v = tio.values.flatten.pack(Termios2::FORMAT)
		self.ioctl(TCGETS2, v)
		tio = Termios2.new(*v.unpack(Termios2::FORMAT))
		if tio.c_ispeed == baud && tio.c_ospeed == baud
			true
		else
			raise "failed to set baudrate expected:#{baud} but set:#{tio.c_ispeed}/#{tio.c_ospeed}"
		end
	end
end

@port = SerialPort.new(
	"/dev/ttyUSB0",
	230400,
	8,
	1,
	0
)
@port.set_encoding(Encoding::BINARY)
@port.set_custom_baudrate(250000)

sleep 0.1

# @port << "\xA5"
loop do
	data = @port.sysread(4096)
	p [data, data.size]
	@port << "\x5A"
end
  1. トップ
  2. tech
  3. ruby-serialport で任意のボーレートを設定するには (ただし Linux に限る)

ioctl に構造体のポインタを渡して値を返してもらうような場合があると思います。

このような場合、文字列つくってそのまま渡せば struct のポインタになるようでした。

つまり pack/unpack("P") を使ってこう書くのは

Termios2::FORMAT = "I!I!I!I!CC19I!I!"
Termios2::FORMAT_POINTER = "P44"

# バッファ
v = tio.values.flatten.pack(Termios2::FORMAT)

# pack("P") でポインタを取得
pointer = [v].pack(Termios2::FORMAT_POINTER)

# ポインタを数値として渡す
self.ioctl(TCGETS2, pointer.unpack("L!")[0])

# unpack("P") でポインタから値を取得し、構造体を unpack
tio = Termios2.new(*pointer.unpack(Termios2::FORMAT_POINTER)[0].unpack(Termios2::FORMAT))

単に文字列を引数に渡す方法で簡単に書ける

# バッファ
v = tio.values.flatten.pack(Termios2::FORMAT)
self.ioctl(TCGETS2, v) # v が ioctl 内で書き変わる
tio = Termios2.new(*v.unpack(Termios2::FORMAT))

v の長さが構造のサイズに足りてないと死にます

備考

リファレンスの IO#ioctl には以下のように書いてあるんだけど、意味がわからなかった……

If it is a string, it is interpreted as a binary sequence of bytes.

  1. トップ
  2. tech
  3. Ruby で IO#ioctl の引数に構造体 (struct) へのポインタを渡したいとき

グラフ化した消費電力値。5:00 ぐらいにエアコンの電源が自動的に入るようになっていて急激に消費電力が増える。起床後は食洗機や洗濯乾燥機などが稼動してさらに増えていく。40A契約だが、一瞬ピークでそこまでいってる (これは原因不明)

節電モニター

節電モニター - はやわかり

はやわかり

5.0 / 5.0

(国内だと節電モニター「はやわかり」という商品名のもの)

500 Can't connect to lowreal.net:443 (certificate verify failed) のとき買って、設置をしてはいたものの、十分に活用していたとはいえない状態でした。電気使いすぎのときにアラートを鳴らしていて、ブレーカーが落ちる1歩手前で気付けるというのが現状では最大の活用でした。

しかし、せっかく USB 端子があってデータが読み出せるのだから、自動的にグラフ化したくなってきました。ちなみにこの製品には Windows 版のかなりデキの悪いソフトウェアが付属していますが、正直常用できるようなものではありません。

製品の詳細を調べる

この製品はどうやら、以下のものと同一のようです。

USB 接続してシステムプロファイラでベンダIDなどを調べて検索すると、Silicon Labs の USB シリアル変換のチップのようでした。


実際プロトコルダンプして読みだしたりしている人がいます。

思ったより情報があったので簡単そうです。

Raspberry Pi で読みだす

ドライバの対応の関係上、Linux で読み出すのが一番簡単です。通常のドライバだと製品IDが登録されていないのですが、Linux の場合はこの製品のIDについても対応するドライバを使うようになっているので、特に苦労せずにシリアル通信ができます。

Raspberry Pi の場合、CP210x のドライバは最初から入っているようなので、特にインストールすることもなく、USB ケーブルを繋ぐだけでシリアルポートとして認識します。

シリアルフォーマット

ボーレート 250000 8bit stopbit 1 parity 0 となっており、ボーレートが特殊なので、場合によって工夫が必要です。

Ruby を使って読みだすことにしましたが、これのせいで自力で ioctl を呼ぶハメになっています。

データ形式

送られてくるのは常に、11バイト固定長で、こちらから送るのは \xA5 か \x5A だけです。解析結果のコードがあるので、その通り実装したらいいだけです。

ただ、フローコントロールをオフにせずに試していたところ、デフォルトだとソフトウェアフローコントロールが有効なようですが、データ欠けが発生して微妙に読みだせず4〜5時間ハマりました。どうやら必ずフローコントロールは完全にオフにする必要があるようです (ハードウェアフローコントロールだとデータ欠けは発生しないが、そもそも応答しなくなりました)

コード

という感じのを Ruby で実装しました。といっても、プロトコル解析はされているし、 Python 実装もあったのでたいしたコードにはなっていません。

https://github.com/cho45/CM160-rb

とにかくフローコントロールが最大のハマリどころでした。

備考

この製品は 315MHz帯の微弱電波機器 (免許・技適不要) となっていて、かなり電波が弱いです。うちはかなり狭い家(マンション)で、玄関上にある分電盤からリビングまで、10mもないと思いますが、これで電波がぎりぎり届きません。

なので、機器同士は比較的近くにおくようにして、Raspberry Pi などでネットに中継するほうが安定して便利そうです。広い家だとむしろ中継が必須な気がします。

とはいえ、そろそろスマートメータの設置もすすむので、こういうのも必要なくなるのかもしれません。

  1. トップ
  2. tech
  3. 市販の節電モニターから値を読みだしてグラフ化

分電盤にセンサーをつけて家庭内全体の消費電力を測るというのはやっていますが、実際のところ何がどう電力を食っているのは大本で計測してもわかりにくいわけです。エアコンや洗濯機などの大きなものはともかく、待機電力の支配率はコンセントごとに電力を常時測定しなければわかりません。

  • コンセントごとにセンサーをつけても十分に安い
  • データを無線で集約できる

みたいなデバイスが欲しくなります。

試作品の回路図

検討はあとに書きますがまず現段階の回路図です (といってもたいして見所はありませんが…)

ただし、

  • ブレッドボードに実装しているのは 1ch のみでアナログスイッチなし
  • 電源回路が回路図だと昇圧回路になっているが消費電力が多すぎるのでやめている

カレントセンサー

交流回路に微小抵抗を入れて直接測定するという手もありますが、これは危険が伴いますし、ちょうどいい微小抵抗の入手が難しいという問題があります。10mΩでも30A流したら抵抗だけで9W消費しますし、15Aでも2.25W消費します。

そうなると、やはりカレントトランスを使うほうが筋が良さそうです。ただカレントトランスもあまり入手性は良くないですし、価格もカットコアのものだとそれなりにします。

参考:http://akizukidenshi.com/catalog/g/gP-08960/

しかし ebay で検索してみると、ちょうど良さそうなカレントトランスが100円程度で売られています。カットコアではありませんが、実際のところカットコアのカレントランスが必要なのは分電盤のように絶対切断できない箇所かつホットとコールドが最初から分かれて配線されている部分だけなので、実質的にはあまり問題にならないでしょう。最終的にはこれを使えばよさそうです。

ただ、とりあえずは2000:1のカットコアのカレントトランスが手元にあるので、これを使います。

ADC部分

カレントセンサーが発生させる微小な電流を電圧に変換してデジタル化します。

センサーの出力電流

通常コンセントは1ポートあたり15Aまでですが、実効値で15Aだと、ピークでは21.2A流れていますし、実際の負荷によってはパルス的に倍以上の電流が流れることもあります。30Aぐらいの負荷まではいけるようにしておきたいところですが、そもそも電力契約上で40Aとか60Aが上限なので、このへんをフルスケールにして計算してもよさそうです。

分解能としては1Wあれば十分すぎるぐらいに感じます。100Vだと0.01Aです。結構なダイナミックレンジになります。

センサーの2次側の出力電流は巻線比によって決まります。1:2000 なら2次側に1次側の2000分の1の電流が流れます。すなわち 30A で 15mA、0.01A で 5uA です。

電圧変換

発生する電流は交流なので、整流する必要があります。整流した電流値を負荷抵抗で電圧に変換し、ローパスフィルタで平均化します。

負荷抵抗は10Ωぐらいにすることが多いみたいです。小さければ小さいほどリニアリティが改善します。これにより、1次側が 30A なら抵抗に最大 212mV(30 / 2000 * 10 * sqrt(2))の電圧がかかる全波整流後の波形になります。

後段にローパスフィルタをつけ、平滑します。このフィルタは整流済み信号の平滑と、ADC 入力前の折り返しノイズ防止フィルタを兼ねています。これにより電圧は元の交流電圧の平均値となります。1次側が 30A なら出力は135mV (30 / 2000 * 10 * 0.9)の直流になります。

0.01A なら45uVです。

ADC

今回は MCP3425 という8倍までのPGAがあるI2C ADCデバイスを使ってみました。16bit、LSB=62.5uV の分解能があり、8倍のPGAがついているので、8倍時にはLSB=7.8125uVとなります。16bit をしっかり生かせるなら、十分な精度となります (が実際のところこのぐらい微小な電圧を測定するのは難しい)

このADCデバイスのフルスケールは±2.048Vですので、60Aで270mVぐらいは余裕です。

本来だと微小な電流検出にはオペアンプを使ったI-V変換回路をつけるのが筋だと思われます。ただ、低オフセット入力なオペアンプが必要になります。低オフセット電圧のオペアンプは高価ですし、今回はこのように ADC デバイスの PGA で8倍までは増幅できるので、まぁなんとかなるだろうという目論みです。

入力はアナログスイッチで複数いけるようにしたい思いがあります。

MCU部分

今回は ESP8266 のみを使うことを考えました。

  • ESP8266 は500円ぐらいで激安
  • WiFi 接続できる MCU として単体でいける

というのが大きなメリットだと感じました。

コード的には MCP3425 から読みだして送信するだけです。省電力のため ESP-NOW という ESP8266 独自のAPIで通信を行っています。

ESP-NOW は受信側が別途必要なので、受信用のデバイスのコードもあります。こちらは STA+AP で起動して、ESP-NOW で受けとったデータをグラフAPIに投げなおしています。

ただ、実際 ESP-NOW を使ってみると、少し離すと、想像以上にフレームが届かないことが多いです。WiFi バンドが混んでいるせいというのもあると思いますが、なかなか厳しく、再送を実装するか、もしくは諦めて普通にWiFi接続したほうがいい気がします。電源次第です…

電源部分

ここが難しくて悩むところです。

もともとは電池駆動しようと思っていましたが、いろいろ実験した結果、電池駆動で半年程度もたせるにはかなりの低頻度のやりとりになることがわかりました。なので、この用途ではあまり筋が良くなさそうです。

一方でAC電源からとってくるのも割と面倒です。200mA 3.3V 程度だけとれる小型のAC/DCスイッチング電源なんて滅多に見ません。ACアダプタを改造するにせよ、そもそもACアダプタがそこそこ高価です。

ということで今のところ電源は保留にしています (モバイルバッテリ駆動しています)

ebay で探すと 3.3V 600mA の AC/DC 電源はすこしヒットしますので、買ってみて使えそうならこれでいい気がします。

現状

ブレッドボードで作業デスク付近のコンセントの電流を計ってみています。まぁまぁ面白いのですが、一箇所だけ計ってもあんまり面白くないです。

電源まわりをうまく解決する方法を考え中です。

その他

本来用途として WiFi は適切なプロトコルとはいえないですから、Bluetooth LE や ZigBee の安価なデバイスがあればそのほうが良いと思います。とはい現状 ZigBee 最安の TWE-Lite ですら ESP-WROOM-02 の3倍ぐらいの値段です。

  1. トップ
  2. tech
  3. コンセントごとの消費電力を知りたい人生

pack のテンプレート文字列から、それを使ってパックした結果のサイズを求めたいということはありませんか。つまりやりたいことは sizeof(struct x) です。

pack テンプレート文字列は割と複雑で、任意長や文字列やポインタなどがあり、全てにおいてうまく動作するものを作るのは無理ですが、だいたいうまくいくのは実装できそうです。

sizeof(template)

def sizeof(template)
	x = Class.new(Numeric) do
		def to_str; ""; end
		def to_int; 0; end
		# implicit to_f is called only with Numeric subclass
		def to_f; 0.0; end
	end.new
	size = template.scan(/([a-zA-Z][_!]?[<>]?)([0-9]*)/).reduce(0) {|r,(_,n)|
		r + (n.empty?? 1 : n.to_i)
	}
	# p [template, size]
	Array.new(size) { x }.pack(template).size
end

やってることは「とりあえず pack してみる」ですが、pack するためには、まずテンプレート文字列に応じて、適当な型と適当な長さの配列が必要になります。

適当な長さという点では多い分には問題ないので、適当に数えています。

適当な型というのは、厳密にやると結局テンプレート文字列をパースするのと同じぐらい面倒なので、pack が要求するデータ型すべてに暗黙的に変換可能なオブジェクトを作っています。

pack が要求する型は整数・文字列・浮動小数点数があります。整数は to_int、文字列は to_str を実装すると、Ruby はそのオブジェクトについて整数・文字列と同等に扱う(必要なら暗黙的に変換される) ことになっています。

浮動小数点数にはそういったどんなオブジェクトも浮動小数点数として扱えるようにする、というメソッド名がないのですが、Numeric のサブクラスであって to_f が実装されている場合には暗黙的に変換されるというルールがあるので、ベースクラスを Numeric にしています。

[
	"C",
	"S",
	"L",
	"Q",

	"c",
	"s",
	"l",
	"q",

	"S_", "S!",
	"I", "I_", "I!",
	"L_", "L!",
	"Q_", "Q!",

	"s_", "s!",
	"i", "i_", "i!",
	"l_", "l!",
	"q_", "q!",

	"S>", "L>", "Q>",
	"s>", "l>", "q>",
	"S!>", "I!>",
	"L!>", "Q!>",
	"s!>", "i!>",
	"l!>", "q!>",

	"S<", "L<", "Q<",
	"s<", "l<", "q<",
	"S!<", "I!<",
	"L!<", "Q!<",
	"s!<", "i!<",
	"l!<", "q!<",

	"n",
	"N",
	"v",
	"V",

	"U",
	"w",

	"D", "d",
	"F", "f",
	"E",
	"e",
	"g",
	"G",

	"A",
	"a",
	"Z",
	"B",
	"b",
	"H",
	"h",
	"u",
	"M",
	"m",

#	"P",
#	"p",
#	"@",
#	"X",
#	"x",

	"C255",
	"i!2s!2",
	"i!i!s!s!",
].each do |tmpl|
	next if tmpl =~ /q/i
	puts "sizeof(%p) = %d" % [tmpl, sizeof(tmpl)]
end
sizeof("C") = 1
sizeof("S") = 2
sizeof("L") = 4
sizeof("c") = 1
sizeof("s") = 2
sizeof("l") = 4
sizeof("S_") = 2
sizeof("S!") = 2
sizeof("I") = 4
sizeof("I_") = 4
sizeof("I!") = 4
sizeof("L_") = 8
sizeof("L!") = 8
sizeof("s_") = 2
sizeof("s!") = 2
sizeof("i") = 4
sizeof("i_") = 4
sizeof("i!") = 4
sizeof("l_") = 8
sizeof("l!") = 8
sizeof("S>") = 2
sizeof("L>") = 4
sizeof("s>") = 2
sizeof("l>") = 4
sizeof("S!>") = 2
sizeof("I!>") = 4
sizeof("L!>") = 8
sizeof("s!>") = 2
sizeof("i!>") = 4
sizeof("l!>") = 8
sizeof("S<") = 2
sizeof("L<") = 4
sizeof("s<") = 2
sizeof("l<") = 4
sizeof("S!<") = 2
sizeof("I!<") = 4
sizeof("L!<") = 8
sizeof("s!<") = 2
sizeof("i!<") = 4
sizeof("l!<") = 8
sizeof("n") = 2
sizeof("N") = 4
sizeof("v") = 2
sizeof("V") = 4
sizeof("U") = 1
sizeof("w") = 1
sizeof("D") = 8
sizeof("d") = 8
sizeof("F") = 4
sizeof("f") = 4
sizeof("E") = 8
sizeof("e") = 4
sizeof("g") = 4
sizeof("G") = 8
sizeof("A") = 1
sizeof("a") = 1
sizeof("Z") = 1
sizeof("B") = 1
sizeof("b") = 1
sizeof("H") = 1
sizeof("h") = 1
sizeof("u") = 0
sizeof("M") = 47
sizeof("m") = 0
sizeof("C255") = 255
sizeof("i!2s!2") = 12
sizeof("i!i!s!s!") = 12
  1. トップ
  2. tech
  3. Ruby の pack テンプレート文字列からそのデータサイズを求める
  1. トップ
  2. ruby
  3. Ruby の pack テンプレート文字列からそのデータサイズを求める

Cの構造体とかだと、構造体の中に他の構造体ということは普通にあります。

こういった構造体の文字列を unpack すると、全部フラットな配列になってしまうので、Ruby レベルのオブジェクト構造として再構成しようと思うと面倒なことになります。

ということで、入れ子に対応した unpack というのを実装してみました。

概要

以下のような感じで {} (ブレース) で入れ子を表現するようにテンプレート文字列を拡張します。

struct_foo_t = %{
	I!
	I!
	Z16
}

struct_bar_t = %{
	I!
	I!
	{
		#{struct_foo_t}
	}
	I!
} #=> "I!I!{I!I!Z16}I!"

original = [
	0xffff,
	0xfeff,
	[
		0x11,
		0x22,
		"foobar"
	],
	0x10,
]

packed = original.pack_deeply(struct_bar_t)
p packed
#=> "\xFF\xFF\x00\x00\xFF\xFE\x00\x00\x11\x00\x00\x00\"\x00\x00\x00foobar\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00"

unpacked = packed.unpack_deeply(struct_bar_t)
p unpacked
#=> [65535, 65279, [17, 34, "foobar"], 16]

p original == unpacked
#=> true

実装

先頭から unpack して入れ子を処理していくというイメージです。処理済みのバイト長が欲しいので、クソみたいですが一旦 unpack したものをもう一度 pack しています。

pack_deeply は単にフラットにして pack するだけなので簡単です。

class String
	def unpack_deeply(template)
		ret = []
		tree = [ ret ]
		n = 0
		tmpl = ""

		# unpack partial and add to result
		unpack = lambda {
			unpacked = self.slice(n..-1).unpack(tmpl)
			# re-pack to get byte length
			size = unpacked.pack(tmpl).size
			n += size
			tmpl = ""
			tree.last.concat(unpacked)
		}

		template.each_char do |chr|
			case chr
			when "{"
				unpack.call
				ary = []
				tree.last << ary
				tree << ary
			when "}"
				unpack.call
				tree.pop
			else
				tmpl << chr
			end
		end
		unpack.call
		ret
	end
end

class Array
	def pack_deeply(template)
		flatten.pack(template.gsub(/[{}]/, ''))
	end
end
  1. トップ
  2. tech
  3. Ruby でネストした構造体文字列をネストした配列として unpack する
  1. トップ
  2. ruby
  3. Ruby でネストした構造体文字列をネストした配列として unpack する

markdown で HTML にして wkhtmltopdf で PDF 化する汎用 Makefile です。

Markdown で書いてプレビューするのはいいんですが、画像がちらばってしまって面倒です。PDF にしておけば1ファイルにまとまるのと、だいたいどこでも見ることができるので便利です。

# brew install markdown
# http://wkhtmltopdf.org/downloads.html

.SUFFIXES: .md .html
.SUFFIXES: .html .pdf

SRCS = ${wildcard *.md}
HTMLS = $(SRCS:.md=.html)
PDFS = $(SRCS:.md=.pdf)

define header
	<!DOCTYPE html>
	<meta charset="utf-8">
	<title>MD</title>
	<style>
	body { font-family: sans-serif; line-height: 1.66 }
	img { max-width: 800px }
	</style>
endef
export header

.PHONY: all
all: $(PDFS)

$(HTMLS): Makefile

.html.pdf:
	wkhtmltopdf $< $@

.md.html:
	(echo "$$header"; markdown $<) > $@
  1. トップ
  2. tech
  3. Markdown → PDF にする Makefile

OS X はビルトインで WiFI のパケットをスニフする機能が入っているので、とりあえずキャプチャして保存するだけならとても簡単にできます。

sudo /usr/libexec/airportd en0 sniff 1

これで WiFi のチャンネル1 (2.5GHz帯) のスニフができ、/tmp 以下に適当なファイルができて保存されます。

キャプチャはこれで良いのですが、別途解析ソフトウェアが必要です。

Wireshark

https://www.wireshark.org/

airportd sniff での結果は Wireshark で解析することができます。.cap ファイルを Wireshark の File → Open… から選択するだけです。

接続先が接続元の Mac アドレスでフィルタする場合、wlan.da == ff:ff:ff:ff:ff:ff や wlan.sa == ff:ff:ff:ff:ff:ff でできます。

該当するようなパケットが見つかっているなら、右クリックでフィルタとして適用とできるようになっています。

WPA2-PSK の解析

WiFi の経路は暗号化されているので、そのままだとIEEE802.11 フレームは読めますが、イーサネットフレームは読めません。しかしこれも Wireshark に適切に設定をすることで解析可能になります。

http://jorisvr.nl/wpapsk.html ここにSSIDとPassphrase を入れてでてくる16進数文字列をコピーしておきます。

Preferences… → Protocols → IEEE802.11 → Decryption Keys

type を wpa-psk として、Key にコピーした16進数文字列を入れます。

これで OK を押すと既存の解析結果も更新され、デコード可能ならデコードされます。

  1. トップ
  2. tech
  3. OS X での WiFi スニファ

オムロン 【自動電源ON】【4秒測定】【体重50g単位表示】【PC/スマホ対応 Wi-Fi通信機能搭載】体重体組成計 カラダスキャン HBF-253W-BK - オムロン(OMRON)

オムロン(OMRON)

5.0 / 5.0

なんとなく欲しいなとは思いつつ高いから買ってませんでしたが、妻が買ってきました。とりあえず結構いい感じです。

セットアップ時にスマートフォンのオーディオ出力とマイク入力を使ってやるようになってました。どういうタイミングで音を出してるのか調べなかったのですが、ヘルプだと「ピポパと聞こえたら…」という記述があるので、DTMF でやってるのかもしれません。このオーディオ出力とマイク入力を使う方法ですが、Zenfone2 ではうまくいった一方で、HTC J One ではうまくいきませんでした (beat audio などは切ってみたんですが…)

それはともかく、WiFi 経由ということですが、実際どのような通信が行われているのか知りたくなってきました。できれば接続先サーバをうまく騙して (ないし透過プロキシを通して) データをインターネットに出る前に自分で処理したいという気持ちもあります。

そんなわけでIEEE802.11フレームのパケットスニファを行って少しだけ追跡してみました。

接続先は vdu.wellnesslink.jp

WiFi パケットの解析をしてみたところ、ちゃんと TLS を使っているようでした。なので透過プロキシやら何やらを挟むことはできません。

体重データは人によってはセンシティブな個人情報ですから、TLS を使うのは当然でしょう。

一方、サーバサイドのサービスが終了してしまったとき、このデバイスの WiFi 機能は完全にゴミになります。自分の所有するデバイスの管理を自分でできないわけですからちょっと気持ち悪いところがあります。

  1. トップ
  2. tech
  3. オムロンのWiFi対応体重計 HBF-253W の通信傍受 (失敗)

「独自拡張のような形でIEEE802.11のフレームを飛ばして」と書きましたが、実際のWiFiパケットは見ておらず、真偽に疑問があったので、キャプチャして確認しました。

結論からいうとその通りのようで、Management frame (ビーコンとかと一緒) を Vendor-specific な形式で使っているようです。Wireshark だと Malformed Packet となってしまうのが謎ですが…


Management frame

Action field

を Vendor-specific で使っている

ただ、Vendor Specific Content にあたる部分に、esp_now_send で渡したバイト列以外にもヘッダみたいなのがついています。これはよくわかりません。

  1. トップ
  2. tech
  3. ESP-NOW はどのように IEEE802.11 フレームを使っているか?

パナソニック 口腔洗浄器 ジェットウォッシャー ドルツ 白 EW-DJ61-W - パナソニック(Panasonic)

パナソニック(Panasonic)

3.0 / 5.0

フロスをどうしてもサボってしまうので、11月ぐらいに買ってみました。だいたい使用感はわかったかなと思うのと、歯科での指導を挟んだのでで感想を書いておきます。

フロスの代わりにはならない

フロスが面倒で買ったのですが、ジェットウォッシャーではフロスの代わりにはなりませんでした。結局歯間の狭いところ(自分はほぼ隣同士の歯がくっついています)の歯垢は落とせないみたいです。かなり念入りにジェットウォッシャーを使ってからでも、フロスを使うとまださらに歯垢がとれます。歯科でのチェックでも、フロス使用時は95%〜ぐらいの歯垢除去率だったのが、ジェットウォッシャーだけの場合80%〜ぐらいまで落ちていました。

歯科のでチェックは歯石(磨き残しが続いて歯垢が固まった状態=恒常的に磨けていない箇所)も見るんですが、サボりがちのフロスでもひどいことにはならないので、サボりがちであってもフロスのほうがマシみたいです。

まぁそりゃそうだろという感じではあります。これは歯間ブラシの代替であって、フロスの代替ではありません。歯間が狭い人には向いてないのでしょう。

食べカスを落とすのは気持ちがいい

食べカスはすごい勢いで落ちてくるので気持ちが良いです。まぁフロスでも落ちるんですが、フロスの場合、糸にヘバりついてくる感じなので使用感が悪いという気持ちの問題があります。

冷い水でやるとつらい

冬の冷い水をすぐに使ってジェットウォッシャーをするとかなりつらいです。知覚過敏の人には苦行になります。

あらかじめて汲んでおいて室温にするとか、お湯でやるとかする必要があります……

今後

とはいえ、やらないよりはマシだと思うので、フロスと併用することにします。結局手順が増えてるだけじゃないかという……

  • VIN 2〜24V
  • VOUT 2〜28V
  • 2A
  • 効率最大95%
  • スイッチング周波数1.2MHz

という表示のもの。

スイッチング周波数が高く、かなり小さい。(スイッチング周波数が高いほどコイルが小さくできるため。ただしスイッチング損が増えるので効率が犠牲になりやすい)。裏面に実装はなく、写っている部品で全て。

注意が必要で、普通と逆(左まわり)で電圧があがる。だから安いのか?

5V → 12V 昇圧での効率

2.5W → 2.2W 88%
5.0W → 4.73W → 94.6%
10W → 8.42W → 84.2%

これ以降出力低下

コイルが結構発熱する。

値段の割に効率が良く、小さいので結構汎用的に使えそう。2V〜 昇圧可能なのも嬉しい。NiMH 2本で昇圧したいときはベストといってもいいんじゃないかという気がする。

SX1308 とは

http://www.suosemi.com/ 深圳市硕芯科技有限公司という会社のもの

オン抵抗 80mΩ パワーMOSFETがビルトインされている。

  1. トップ
  2. tech
  3. 超小型 DC/DC ステップアップコンバータ SX1308 2A $0.99