ESP32を使って、Amazonで安価に売られている64x32ピクセルのLEDマトリクスに色々表示できるようにしてみました。
使ったもの
・P3 RGBピクセルパネルHDビデオディスプレイ64×32ドットマトリックスSMD LEDディスプレイモジュール192×96mm
あとは適宜ブレッドボードなど。
ESP32を使って、Amazonで安価に売られている64x32ピクセルのLEDマトリクスに色々表示できるようにしてみました。
・P3 RGBピクセルパネルHDビデオディスプレイ64×32ドットマトリックスSMD LEDディスプレイモジュール192×96mm
あとは適宜ブレッドボードなど。
Bluetooth接続ができるUSB電圧・電流テスター「UM24C」を買ってみました。
公式のAndroid端末やWindows用のソフトを使って値を読み出すことができます。が、自作のプログラムからこれを読み出してみました。
カラーLCDがついていてなかなか派手ですが見やすい。上下に左右2つのボタンがついていて、これで操作します。操作HELP表示(英語)つき。これ単体で電圧や電流変化のグラフを見ることもできたりして、なかなか高機能です。
慣れてる人ならAliExpressで購入した方が安く買えます。Alibaba グループ | AliExpress.comの 電圧メートル からの UM24C 2.0カラー液晶ディスプレイusb電圧テスター電流計電圧計amperimetroバッテリー充電ケーブル抵抗30% 中の UM24C 2.0カラー液晶ディスプレイusb電圧テスター電流計電圧計amperimetroバッテリー充電ケーブル抵抗30%
↑ UM24 (Cがつかない) も選べますが、そちらはBluetooth通信機能がありませんのでご注意を。購入から1週間くらいで届きました。
※ 上位版?のUM25Cというのも出ているようですね。USB Type-Cボートがついているらしい。
USB電源につないだ状態でBluetoothデバイスを表示させてみると、普通のSPP(シリアルボートプロファイル)として見えます。
Windowsマシンでは、UM24Cを追加すると、PINを聞かれ、「1234」を入力すれば接続できます。COMボートが2つ追加され、どちらか一方に接続して利用します。公式のソフトがあり、これを導入すれば良いのですが、これがNational Instrumentsの計測プラットフォーム(NI-VISA)が丸っと入るため、かなり巨大です。
Macではペアリング自体は簡単にできるのですが、追加されるPort ( /dev/tty.Port-UM24C )に接続してもデータが読めず。(Windowsでも2つのうち1つのCOMポートは全く応答がないので、これだけが見えてしまっているのでしょうか…?)
USB-Bluetoothアダプタを接続したLinuxボード (Debianで試しました) からは以下の手順で接続できました。
$ sudo apt install bluez bluez-utils # 必要なパッケージの導入
$ sudo bluetoothctl -a
・デバイスをスキャン
[bluetooth]# scan on
[NEW] Device 00:BA:55:XX:XX:XX UM24C
・発見されたUM24Cをペアリング
[bluetooth]# pair 00:BA:55:XX:XX:XX
Attempting to pair with 00:BA:55:XX:XX:XX
Request PIN code
[agent] Enter PIN code: 1234
[CHG] Device 00:BA:55:XX:XX:XX UUIDs: 00001101-0000-1000-8000-00805f9b34fb
[CHG] Device 00:BA:55:XX:XX:XX Paired: yes
Pairing successful
[bluetooth]# trust 00:BA:55:XX:XX:XX
[CHG] Device 00:BA:55:XX:XX:XX Trusted: yes
Changing 00:BA:55:XX:XX:XX trust succeeded
・rfcommで接続
$ sudo rfcomm bind 0 00:BA:55:XX:XX:XX
これで、 /dev/rfcomm0 として UM24C に接続できるようになりました。
あとは、適当なプログラムでこのポートを開き、「0xf0」の1バイトを送信すると、130バイトほどのレスポンスが返ってきます。なお jerm コマンドは jerminal というターミナル接続ソフトです。
$ (printf "\xf0"; sleep 2) | jerm /dev/rfcomm0 | xxd
...
00000000: 0963 0207 000a 0000 0033 0016 0048 0000 .c.......3...H..
00000010: 0000 020b 0000 09da 0000 0003 0000 000f ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0030 0030 0000 0000 0000 0000 0000 000a .0.0............
00000070: 0000 0000 0000 0001 0002 0000 1446 0000 .............F..
00000080: fff1 ..
16bit Big Endian値が並んでいる感じで、最初は固定値?0x963、
電圧 0x207 = 519 -> 5.19V
電流 0x0a = 10 -> 0.010 A
…
他にも電流積算値や抵抗値など、画面上に出ているものは大体そのまま全て並んでいることがわかります。
ちなみに、他にも「0xf1」を送ると画面切り替え、「0xf2」を送ると画面回転、「0xf3 」「0xf4」で積算値グループの切り替えやクリアができたりします。
Rubyでログを記録するプログラムを書いてみました。
#!/usr/bin/ruby
require 'serialport'
require 'timeout'
require 'json'
Signal.trap(:PIPE) { exit }
sp = SerialPort.new('/dev/rfcomm0')
sleep 2
failure = 0
loop do
sp.write "\xf0"
begin
Timeout.timeout(2) do
s = sp.read(130)
if !s || s.length < 130
warn 'incomplete data'
failure += 1
next
end
data = s.unpack('n*')
# p data
T: Time.now,
V: data[1]/100.0,
A: data[2]/1000.0,
W: data[4]/1000.0,
C: data[5],
mAh: 256 * data[8] + data[9],
mWh: 256 * data[10] + data[11],
ohm: data[62]/10.0,
})
puts json
STDOUT.flush
failure = 0
end
rescue Timeout::Error
warn 'timeout'
failure += 1
end
if failure > 5
warn 'Too many failure. quit.'
exit 1
end
sleep 1
end
これを実行すると1秒ごとにJSON形式で計測値が出力されます。
{"T":"2018-04-14 23:01:48 +0900","V":5.17,"A":0.089,"W":0.46,"C":21,"mAh":6,"mWh":31,"ohm":58.0}
{"T":"2018-04-14 23:01:49 +0900","V":5.17,"A":0.089,"W":0.46,"C":21,"mAh":6,"mWh":31,"ohm":58.0}
{"T":"2018-04-14 23:01:50 +0900","V":5.17,"A":0.089,"W":0.46,"C":21,"mAh":6,"mWh":31,"ohm":58.0}
...
これを使ってiPhone Xの充電中の電流などを計測してみました。
なお、iPhoneのバッテリー残量は、MacにiPhoneを接続した状態で libimobiledevice を使って取得することができます。
$ brew install libimobiledevice # インストール
$ idevice_id -l
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # <- IDを確認
$ ideviceinfo -u XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -q com.apple.mobile.battery
BatteryCurrentCapacity: 96 # <- バッテリー残量
BatteryIsCharging: false
ExternalChargeCapable: false
ExternalConnected: false
FullyCharged: false
GasGaugeCapability: true
HasBattery: true
これを定期的に記録して、UM24Cで測った計測値と合わせてグラフ化してみました。
以下が残り5%の状態から100%までの、2時間半の充電中の状況です。
ちょっと単位が入り混じってしまっていますが、92-93%くらいまで充電されたところで充電電流がカクッと減る様子などがみて取れます。
なお、電流積算値はiPhoneアプリ 「Battery Care」を使って取得した最大容量 2700mAhと結構ぴったり一致しました。(iPhone Xの最大容量は2800mAhですが、使っているうちに僅かに劣化したようです。) とはいえ充電中も電源はONなので、iPhoneの消費と誤差がたまたま相殺したのでしょうかね。
赤外線アレイセンサ AMG8833、いわゆるGrid-EYEを使ってみました。
上記の説明の通り、60度の四角錐の測定エリア(2次元)を8x8に分割した64個のピクセルについて、0〜80℃の温度が得られます。時間分解能は10fpsです。
これを、ESP8266 (ESP-WROOM-02) のI2Cで読み出してみました。上記は3.3Vで動作するモジュールになっているので、電源を含め、4本(Vcc, GND, SCL, SDA)の線で繋ぐだけで動作します。
ESP8266の場合、ArduinoのWireライブラリはデフォルトでSDAにGPIO4、SCLにGPIO5を使用するため、この通りにしておくとスムーズです。(これ以外のピンでもピン番号さえ指定すれば動作します。)
ESP8266はこちらのブレークアウトボードを使いました。
ESP-WROOM-02-搭載mikroBUS-R-対応ブレークアウトボード-Ver-2
基盤は以前別の目的でFusionPCBで作ったものを流用しています。GPIO13にはステータス確認用のLEDがついています。
AMG8833が取得した10fpsの結果をリアルタイムに手軽に可視化して見るため、WebSocketでブラウザにデータを流して表示してみることにします。ESP8266上にWebサーバ/WebSocketサーバを乗せて、そこにWebブラウザで接続すれば見られるようにしましょう。
AMG8833に対応したArduino用のライブラリはいくつか公開されていますが、今回はAdafruitが公開しているものを使いました。
Arduino IDEの「スケッチ → ライブラリをインクルード → ライブラリを管理…」をメニューから選び、Adafruit AMG88xx Libraryを検索してインストールします。ついでにWebSockets というライブラリも入れておきます。これは名前の通りWebSocketのサーバ/クライアント実装です。
あとは記事末尾のプログラムをArduino IDEで書き込みます。
AMG8833に関連するのは以下の部分です。0x68はこのセンサのI2Cアドレスです(0x68と0x69を基盤上のジャンパで選択可能。Adafruitのライブラリではデフォルトが0x69になっていました。)
#include <Wire.h>
#include <Adafruit_AMG88xx.h> Adafruit_AMG88xx amg; // setup amg.begin(0x68); // loop float pixels[AMG88xx_PIXEL_ARRAY_SIZE]; amg.readPixels(pixels);
ESP8266を起動すると、指定したWi-Fiに接続し(接続中は1秒間隔でLEDが点滅し、接続成功すると3秒間隔の点滅になります)、Webサーバが起動します。シリアル出力またはArduino IDEの「ツール → シリアルボート」でIPアドレスを確認し、そこにWebブラウザで「 http://192.168.10.xx/ 」のように接続すると、以下のように8x8のグリッドにリアルタイムの出力値が表示されます(なお自分の方に向けたときに鏡像になるよう、左右反転して表示させています)。グリッド内の各数値は℃、色は0℃〜30℃に色相を適当に割り当てています(JavaScriptの bgcolor 関数)。
近距離なら人の形が識別できそう。
センサから2mくらい離れたところで手を振ってみたところ。10fpsだと結構動きもわかります。
暖房をつけている状態で部屋全体が入るくらいの距離にしてみると、天井近くの方が床付近よりも2〜3℃くらい暖かいことがわかったりします。なおここで見ているのは空気の温度ではなく、壁の温度だと思います。
何かコードを書いて、ブランチをGitHubにpushしたあと、そのブランチのプルリクエストをさくっと作りたかったので、1コマンドで現在のブランチからのPull Request作成ページを開くコマンドを作りました。(共有レポジトリを想定)
以下のコードを実行可能権限をつけてパスの通った場所に git-ghpr という名前で置き、
git ghpr
とするだけでOK(なおブラウザを開くopenコマンドはMacの前提です)。ghprは GitHub Pull Request の略のつもりです。
perl で何やらやっているのは、親の最も近いブランチ名からPull Request先のブランチを推定する処理です。常に master 等に出すと決まっている場合は、ORIGIN="master" 等として固定値を入れておくと良いでしょう。
#!/bin/bash set -e ORIGIN_URL="$(git config --get remote.origin.url)" case "$ORIGIN_URL" in "git@github.com:"*) GHPATH="${ORIGIN_URL#*:}" GHSRC="https://github.com/${GHPATH%.git}" ;; "ssh://git@github.com/"*) GHPATH="${ORIGIN_URL#*@}" GHPATH="${GHPATH#*/}" GHSRC="https://github.com/${GHPATH%.git}" ;; "https://github.com/"*) GHSRC="$ORIGIN_URL" ;; *) echo "origin url is not github" exit 1 esac # Pull Request先を推定 ORIGIN="$(git describe --exclude \*/HEAD --all @~ | perl -pe 's/(?:heads|remotes|origin)\/|-\d+-g[\da-f]+$//g')" GHURL="$GHSRC/compare/$ORIGIN...$(git rev-parse --abbrev-ref @)" echo "$GHURL" open "$GHURL"
Qiでは受電機器から充電器に対して通信を行うことで、充電の開始/停止や電力の調整を行いながら動作しています。これにより異常があれば即時充電を止めるようになっているわけですね。
通信速度は2kbpsで、受電側の負荷を変化させることで充電側に電力の変化として通信データを伝える後方散乱変調という仕組みが使われています。詳しくは以下のデータシートなどが参考になります。
https://toshiba.semicon-storage.com/info/docget.jsp?did=14812&prodName=TC7763WBG
実際にどんな通信がなされているか気になったので、この通信パケットをキャプチャしてみました。
Macのターミナル上でRubyのpryを使っている際、Ctrl-Kなどでカットした文字列を貼り付けようとCtrl-Yをタイプすると、サスペンドしてしまうことがあります。ちなみにirbだと発動しません。
$ pry [1] pry(main)> [^Yをタイプ] [1]+ Stopped pry $
これはDSUSPという機能によるもの。Ctrl-Z(susp)が入力されたら直ちにジョブをサスペンドさせるのに対し、Ctrl-Y(dsusp)はこのキーで入力された制御文字をプロセスがreadしたタイミングでジョブをサスペンドさせるというもので、BSD互換の環境のみで利用可能な機能だそうです。
https://www.gnu.org/software/libc/manual/html_node/Signal-Characters.html
Mac上でsttyを使ってキー割り当てを調べてみると、
$ stty -a speed 9600 baud; 60 rows; 80 columns; ... cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>; eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V; min = 1; quit = ^\; reprint = ^R; start = ^Q; status = ^T; stop = ^S; susp = ^Z; time = 0; werase = ^W;
dsusp = ^Y となっているのが分かります。
Liunx環境では発生しないこともあり、よくMacでだけハマって面倒なので、dsuspを無効化します。
無効化するには
stty dsusp undef
を実行すればOK。自動化するなら .bashrc とか .zshrc に
tty >/dev/null && stty dsusp undef
などと書いておけば良いでしょう。
巨大なテキストデータを標準入力(pipe等)から受け取り、1行ずつ何か処理をして、結果を標準出力(これもpipe等)に書くプログラムを書こうとしてハマるパターン。
(なお文字コードのことはここでは忘れたことにするので、別途対応が必要かもしれない。)
例えば、ssh remote-host cat huge-file.txt | node process-data.js | xz -c > compressed-data.xz
などと使うような想定。
var chunk; while(chunk = process.stdin.read()) process.stdout.write(`data: ${chunk}`); }
{ Error: EAGAIN: resource temporarily unavailable, read errno: -35, code: 'EAGAIN', syscall: 'read' }
process.stdin.fdは非同期openされたfdを返すため、fs.readするとpipeにデータが来なくなった瞬間、例外になる。
process.stdin.on('readable', () => { var chunk = process.stdin.read(); if (chunk !== null) { process.stdout.write(`data: ${chunk}`); } });
一見うまく行くように見える。というかAPIドキュメントに載っているソースである。が、書き出し先がストールすると、見る見るうちにnode.jsのメモリ使用量が増加していく(node echo.js < /dev/urandom | sleep 99999 などとして試してみれば良い)。最後には
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory
お亡くなりになる。
fdは、実は単に 0 と指定すれば同期readとなる(というかshellがopen(2)したfdがそのまま使われる)。しかし…
const fs = require("fs"); fs.readFile(0, function (err, data) { // ... 処理 & 書き出し }
これも全データを一度に読み込もうとしてしまうため、メモリが枯渇する。少しずつ読み込もう。
const fs = require("fs"); var BUFFER_SIZE = 4096; var buffer = new Buffer(BUFFER_SIZE); while (true) { var n = fs.readSync(0, buffer, 0, BUFFER_SIZE); if (n <= 0) return null; // bufferを使って何か処理して結果を出力。ここではbufferをそのまま出力する。 fs.write(process.stdout.fd, buffer); }
またしてもメモリが枯渇する。fs.writeはどんどんメモリにwriteすべきものを積んでしまうのだ…。process.stdout.writeも同じである。かといってfs.writeSyncにすると、
Error: EAGAIN: resource temporarily unavailable, write
pipeがストールした瞬間、例外。process.stdout.fdも非同期openされたfdなのである。
fs.writeSyncにして、かつfdとして 1 を渡そう。
const fs = require("fs"); var BUFFER_SIZE = 4096; var buffer = new Buffer(BUFFER_SIZE); while (true) { var n = fs.readSync(0, buffer, 0, BUFFER_SIZE); if (n <= 0) return null; // buffer.slice(0, n)を使って何か処理して結果を出力。ここでは入力をそのまま出力する。 fs.writeSync(1, buffer.slice(0, n)); }
1行ずつstdinを同期的に読んで返すreadline関数を作ってみる。改行で入力チャンクが切れるとは限らないので、チャンク同士を繋いでやる処理が必要になる。
const fs = require("fs"); var readline = (function() { var BUFFER_SIZE = 65536; var buffer = new Buffer(BUFFER_SIZE); var lines = []; return function() { if (lines.length > 1) return lines.shift(); while (true) { var n = fs.readSync(0, buffer, 0, BUFFER_SIZE); if (n <= 0) return lines[0] || null; var newlines = buffer.slice(0, n).toString().split("\n"); if (lines.length == 1) lines[0] += newlines.shift(); lines = lines.concat(newlines); if (lines.length > 1) { return lines.shift(); } } }; })(); var line; while ((line = readline()) != null) { fs.writeSync(1, line + "\n"); }
BUFFER_SIZEはデフォルトのパイプバッファサイズである64KBにしてみた。
しかし、これはC言語か何か?というような代物になってしまった。node.jsのプログラミングパラダイムでなんとかしたい。
Streamクラスの pipe を使えばバッファをいい感じに管理してくれて、入力にバックプレッシャーを伝えることができる。処理部分はstream.Transformを使ってTransformStreamとして実装する。
var stream = require('stream'); // 大文字に変換する var transform = new stream.Transform({ transform: function (chunk, encoding, callback) { callback(null, chunk.toString().toUpperCase()); } }); process.stdin .pipe(transform) .pipe(process.stdout);
1行ずつ改行区切りで処理するなら、byline npmが便利。
LineStreamをpipeで挟むだけで、改行区切りで送ってくれるようになる。
空行は消えるので注意。維持したければ LineStreamのコンンストラクタに {keepEmptyLines: true} を指定すれば良い。
var stream = require('stream'); var LineStream = require('byline').LineStream; var lineStream = new LineStream(); var transform = new stream.Transform({ transform: function (chunk, encoding, callback) { callback(null, chunk.toString().toUpperCase() + "\n"); } }); process.stdin .pipe(lineStream) .pipe(transform) .pipe(process.stdout);
問題点:Cっぽいやつより4倍遅い。。バッファの量の設定等で早くなるかは不明。