赤外線アレイセンサ 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 の略のつもりです。

#!/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}" ;;
  "https://github.com/"*)
    GHSRC="$ORIGIN_URL" ;;
  *)
    echo "origin url is not github"
    exit 1
esac
GHURL="$GHSRC/compare/master...$(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倍遅い。。バッファの量の設定等で早くなるかは不明。

プロンプトのgitブランチ表示を定期更新

Gitのブランチ名をzshのプロンプトに表示させているのですが、別端末でブランチを切り替えたりすると、普通は現状が反映されません。心配だったらenterキーを押せば良いのですが、端末がたくさん開いてるときに限って間違ったブランチで作業してしまいそうになるので、これを防ぐために1分に1度プロンプトを更新するようにしました。ついでにプロンプトの時刻も更新されます。


関係ありそうな .zshrc の部分を切り出すと、こんな感じ。補完候補(menu select)表示中に更新が起こると候補が消えてしまうという課題がありますが…

zmodload zsh/datetime # $EPOCHSECONDS, strftime等を利用可能に

#gitブランチ名表示
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats '%c%u%b'
zstyle ':vcs_info:git:*' actionformats '%c%u%b|%a'

#git情報更新
update_vcs_info() {
  psvar=()
  LANG=en_US.UTF-8 vcs_info
  [[ -n "$vcs_info_msg_0_" ]] && psvar[1]="$vcs_info_msg_0_"
}

#プロンプトを毎分更新
reset_tmout() { TMOUT=$[60-EPOCHSECONDS%60] }
precmd_functions=($precmd_functions update_vcs_info reset_tmout)
redraw_tmout() { zle reset-prompt; reset_tmout }
TRAPALRM() { update_vcs_info; redraw_tmout }

#プロンプト
unsetopt promptcr               # 改行のない出力をプロンプトで上書きするのを防ぐ
setopt PROMPT_SUBST
PROMPT="%F{green}[%m-%T]%f%# "
RPROMPT="%(?..%F{red}-%?-)%F{green}[%1(v|%F{yellow}%1v%F{green} |)%n:%~]%f"
[[ -n "$SSH_CLIENT" ]] && PROMPT="%F{green}[%F{cyan}%B%m%b%F{green}-%T]%f%# "
追記

menuselectが消えてしまう件は、以下のように$WIDGET (直前に入力したキーのアクション) と$_lastcomp[insert] (最後に表示した補完の情報)の値を調べて、menu selectが表示されていると思われる場合にはresetを止めることで回避できそうです。なお該当する $WIDGET の値はキーバインドによって変わる可能性があります。自分の環境で何にすべきかは bindkey "^I" でTabキーに割り当てられているアクションを調べれば分かります。
ただし、古いzsh(5.1未満)でTRAPALRMの中で$WIDGETを評価しようとするとSEGVで死んでしまうバグがあるようで、そのような環境では諦めるしかありません。。

autoload -U is-at-least
precmd_functions=($precmd_functions reset_tmout reset_lastcomp)
reset_lastcomp() { _lastcomp=() }
if is-at-least 5.1; then
    # avoid menuselect to be cleared by reset-prompt
    redraw_tmout() {
        [ "$WIDGET" = "expand-or-complete" ] && [[ "$_lastcomp[insert]" =~ "^automenu$|^menu:" ]] || zle reset-prompt
        reset_tmout
    }
else
    # evaluating $WIDGET in TMOUT may crash :(
    redraw_tmout() { zle reset-prompt; reset_tmout }
fi
TRAPALRM() { check_gitinfo_update; redraw_tmout }

...
追記ここまで


なお変更の有無を表示する check-for-changes は巨大レポジトリだとあからさまに遅くなるので入れてません。


しかし、環境によって vcs_info が遅いことがあるので、ディレクトリやgit HEADが移動された時以外は実行しないようにし、さらにそのチェックも別のプロセスを立ち上げたりすることなくzshのプロセス内で処理が完結するようにした版が以下。


やってることは、precmdでコマンドを記録しておいて、gitが実行されたりディレクトリ移動が起きたらvcs_info、それ以外でも、各コマンド実行後と1分ごとに.git/HEADの更新日時を見て、前回のチェック時刻から更新が起きていたらvcs_info、という感じです。更新日時のチェックはextended_globの更新日時を元にマッチさせる機能を使っているので、外部コマンドの呼び出したりはしない(つまりforkしない)ため高速なはず。

続きを読む

flymakeでrubocopを環境に合わせて実行

rubyを書く際にコーディングスタイルをrubocopでチェックしたい時、手動でチェックしていると忘れることがあるので違反があったらその場でエディタに警告して欲しいわけですが、これをemacsのflymakeでやる方法のメモ。

.emacsなどをいろんな環境で共有しているので、マシンによってrubocopが入っていたりなかったり、あってもグローバルでなく個々のbundleに入っていたりといった状況に対応できるようにします。(rubocopがない場合はruby -cによりシンタックスチェックのみ行います。)
(flymakeよりflycheckの方がいいのかもしれないですが、パッケージ等を追加で入れなくてもemacsについてくることを重視してflymakeにしてます。なんか時代錯誤 *1 )


まず、下記のshell scriptを ~/bin/ とか適当な場所に置きます。PATHが通ってなくても構いませんが、権限は実行可能にしておきます。

  • cat bin/flymake-ruby.sh
#!/bin/sh
RUBOCOP="rubocop --format emacs"

exists () {
    which "$1" >/dev/null 2>&1
}

# use rubocop directly
exists rubocop && exec $RUBOCOP "$@"

# use rubocop in the bundle
CWD="$PWD"
abspath="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
until [ "$PWD" = "/" ]; do
    if [ -f Gemfile.lock ]; then
        if grep rubocop Gemfile.lock >/dev/null; then
            exec bundle exec $RUBOCOP $abspath 2>&1
        fi
        break
    fi
    cd ..
done
cd "$CWD"

# if no rubocop is available, just use ruby syntax check
ruby -c "$@"


あとは .emacs に、flymakeでrubyスクリプトの編集中にflymake-ruby.shでチェックをかけて、違反があれば赤色表示になるように設定を追加します。以下では M-n / M-p キーでエラーのある位置に飛ぶとともに、エラー内容をminibufferに表示するようにしています。
(ruby-modeやflymake自体の設定などは適宜環境に合わせてください。なお以下ではバッククォートが \ (円マーク) になってしまっていますのでご注意を。)

;; ruby mode
(autoload 'ruby-mode "ruby-mode" nil t)
;(autoload 'ruby-mode "ruby-electric" nil t)
(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))

;; flymake
(require 'flymake)
(global-set-key "\C-cd" 'flymake-popup-current-error-menu)
(global-set-key "\M-n" 'flymake-goto-next-error)
(global-set-key "\M-p" 'flymake-goto-prev-error)
(defun display-error-message ()
  (message (get-char-property (point) 'help-echo)))
(defadvice flymake-goto-prev-error
    (after flymake-goto-prev-error-display-message) (display-error-message))
(defadvice flymake-goto-next-error
    (after flymake-goto-next-error-display-message) (display-error-message))
(ad-activate 'flymake-goto-prev-error 'flymake-goto-prev-error-display-message)
(ad-activate 'flymake-goto-next-error 'flymake-goto-next-error-display-message)

;; flymake for ruby
(defun flymake-ruby-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-file  (file-relative-name
                       temp-file
                       (file-name-directory buffer-file-name))))
    (list "~/bin/flymake-ruby.sh" (list local-file))))
(push '(".+\\.rb$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '(".+\\.rake$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("Rakefile$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("^\\(.*\\):\\([0-9]+\\): \\(.*\\)$" 1 2 nil 3) flymake-err-line-patterns)
(push '("^\\(.*\\):\\([0-9]+\\):[0-9]+: \\(.\\): \\(.*\\)$" 1 2 3 4) flymake-err-line-patterns)
(add-hook 'ruby-mode-hook
          '(lambda ()
             ;; Don't want flymake mode for ruby regions in rhtml files and also on read only files
             (if (and (not (null buffer-file-name)) (file-writable-p buffer-file-name))
                 (flymake-mode t))
             ))


rubocop自体の設定は RuboCopの設定アレコレ - Qiita などを参考に。

*1:そもそもemacsが…