ESP32で64x32 LEDマトリクスパネルを制御

ESP32を使って、Amazonで安価に売られている64x32ピクセルのLEDマトリクスに色々表示できるようにしてみました。

f:id:NeoCat:20180502031918j:plain

 
ESP32 + 64x32 RGB LED Matrix 

使ったもの

ESP32-DevKitC

P3 RGBピクセルパネルHDビデオディスプレイ64×32ドットマトリックスSMD LEDディスプレイモジュール192×96mm 

 f:id:NeoCat:20180502205034p:plain

あとは適宜ブレッドボードなど。 

配線

続きを読む

Bluetoothで値が読めるUSB電圧・電流モニター「UM24C」を使ってiPhone Xの充電状況を測ってみる

Bluetooth接続ができるUSB電圧・電流テスター「UM24C」を買ってみました。

公式のAndroid端末やWindows用のソフトを使って値を読み出すことができます。が、自作のプログラムからこれを読み出してみました。

 

f:id:NeoCat:20180414222359p:plain

カラーLCDがついていてなかなか派手ですが見やすい。上下に左右2つのボタンがついていて、これで操作します。操作HELP表示(英語)つき。これ単体で電圧や電流変化のグラフを見ることもできたりして、なかなか高機能です。

入手方法

Amazonでも購入できます

慣れてる人なら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                                                                                      

      json = JSON.generate({

        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 Xの充電中の電流などを計測してみました。

なお、iPhoneのバッテリー残量は、MaciPhoneを接続した状態で 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時間半の充電中の状況です。

f:id:NeoCat:20180414230918p:plain

ちょっと単位が入り混じってしまっていますが、92-93%くらいまで充電されたところで充電電流がカクッと減る様子などがみて取れます。

なお、電流積算値はiPhoneアプリ 「Battery Care」を使って取得した最大容量 2700mAhと結構ぴったり一致しました。(iPhone Xの最大容量は2800mAhですが、使っているうちに僅かに劣化したようです。) とはいえ充電中も電源はONなので、iPhoneの消費と誤差がたまたま相殺したのでしょうかね。

 

 

赤外線アレイセンサ AMG8833 (Grid-EYE) をWebSocketで見てみる

赤外線アレイセンサ AMG8833、いわゆるGrid-EYEを使ってみました。

www.switch-science.com

 

上記の説明の通り、60度の四角錐の測定エリア(2次元)を8x8に分割した64個のピクセルについて、0〜80℃の温度が得られます。時間分解能は10fpsです。

これを、ESP8266 (ESP-WROOM-02) のI2Cで読み出してみました。上記は3.3Vで動作するモジュールになっているので、電源を含め、4本(Vcc, GND, SCL, SDA)の線で繋ぐだけで動作します。

ESP8266の場合、ArduinoWireライブラリはデフォルトでSDAにGPIO4、SCLにGPIO5を使用するため、この通りにしておくとスムーズです。(これ以外のピンでもピン番号さえ指定すれば動作します。)

 ESP8266はこちらのブレークアウトボードを使いました。

ESP-WROOM-02-搭載mikroBUS-R-対応ブレークアウトボード-Ver-2

 

基盤は以前別の目的でFusionPCBで作ったものを流用しています。GPIO13にはステータス確認用のLEDがついています。

f:id:NeoCat:20180128185609j:plain

 

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 関数)。

 

近距離なら人の形が識別できそう。 

f:id:NeoCat:20180128210415p:plain

 

センサから2mくらい離れたところで手を振ってみたところ。10fpsだと結構動きもわかります。

f:id:NeoCat:20180128205653g:plain

 

 

暖房をつけている状態で部屋全体が入るくらいの距離にしてみると、天井近くの方が床付近よりも2〜3℃くらい暖かいことがわかったりします。なおここで見ているのは空気の温度ではなく、壁の温度だと思います。 

f:id:NeoCat:20180128210420p:plain

 

プログラム

 

gist.github.com

GitHubのPull Reqest作成ページをコマンド1つで開く

何かコードを書いて、ブランチを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のパケットを解析してみる

iPhoneにも 8 / X でQiが搭載されました。

Qiでは受電機器から充電器に対して通信を行うことで、充電の開始/停止や電力の調整を行いながら動作しています。これにより異常があれば即時充電を止めるようになっているわけですね。

通信速度は2kbpsで、受電側の負荷を変化させることで充電側に電力の変化として通信データを伝える後方散乱変調という仕組みが使われています。詳しくは以下のデータシートなどが参考になります。

https://toshiba.semicon-storage.com/info/docget.jsp?did=14812&prodName=TC7763WBG

 

実際にどんな通信がなされているか気になったので、この通信パケットをキャプチャしてみました。

回路

続きを読む

pryがCtrl-Yでサスペンドしてしまう

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

などと書いておけば良いでしょう。

Node.jsでUNIX-likeにパイプ処理

巨大なテキストデータを標準入力(pipe等)から受け取り、1行ずつ何か処理をして、結果を標準出力(これもpipe等)に書くプログラムを書こうとしてハマるパターン。
(なお文字コードのことはここでは忘れたことにするので、別途対応が必要かもしれない。)


例えば、ssh remote-host cat huge-file.txt | node process-data.js | xz -c > compressed-data.xz などと使うような想定。


ダメな例1:
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にデータが来なくなった瞬間、例外になる。

ダメな例2:
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

お亡くなりになる。

ダメな例3:

fdは、実は単に 0 と指定すれば同期readとなる(というかshellがopen(2)したfdがそのまま使われる)。しかし…

const fs = require("fs");
fs.readFile(0, function (err, data) {
  // ... 処理 & 書き出し
}

これも全データを一度に読み込もうとしてしまうため、メモリが枯渇する。少しずつ読み込もう。

ダメな例4:
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 APIを使う

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倍遅い。。バッファの量の設定等で早くなるかは不明。