M5 ATOM開発日記その2:Wi-Fiに固定IPアドレスで繋いでみる

ATOM弄りシリーズ、次はWi-Fiに繋いでみます。
昔々はスタンドアロンマイコンからデータを取り出すというのは中々大変で、Arduino用のWi-FiモジュールやBluetoothモジュールは結構良いお値段がする物でした。それが今や1000円ちょいの小さなユニットの中に全部内蔵されてるんですから、いい時代になった物です。

今回はいきなりコードどーん。

#include <M5Atom.h>
#include <WiFi.h>
#include <HTTPClient.h>

const char* SSID = "s-s-i-d";
const char* WIFI_PASS = "password";
const IPAddress IP(192, 168, 0, 2);
const IPAddress GATEWAY(192, 168, 0, 1);
const IPAddress NETMASK(255, 255, 255, 0);
const IPAddress DNS(192, 168, 0, 1);

const char* URI = "http://httpstat.us/200";

HTTPClient http;

void setup() {
  M5.begin(true, false, true);

  if (!WiFi.config(IP, GATEWAY, NETMASK, DNS)) {
    Serial.println("Failed to configure!");
  }
  WiFi.begin(SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
  }
}

void loop() {
  http.begin(URI);
  int httpCode = http.GET();
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    Serial.println(payload);
  } else {
    Serial.println("HTTP GET Error.");
  }
  http.end();

  delay(60000);
}

なんとなく分かるかと思いますが、デバイスIPアドレス固定でWi-Fiに接続し、指定したURLのコンテンツを取ってくるコードです。なぜIPアドレスを固定したいかというと、デバイスに対してpingを打って疎通を確認したいんですね。
今のSwitchbotの温度計でのデータ測定での不満は、デバイス自体の死活を監視できないこと。勿論データの欠測が発生したら落ちたと見なすというのでも代用可能なんですが、直接監視したい、というのがやはり本望でして。

で、別途温度センサーなんかのテストもしたので、そろそろデータをどうやって吸い上げるか、を考える必要が出てきました。
今の流行はMQTTプロトコルで吸い上げるのっぽいので、ちょっと環境を整えつつ色々試してみようかと思います。

M5 ATOM開発日記その1:まずはLチカ

今までは自宅の温度計測をSwitchbotの温度計でやっていたわけですが、ぼちぼち自分で作ったデバイスでやりたいなぁという気持ちになりまして、M5Stackシリーズの一番小さなマイコンバイスであるATOMシリーズでいっちょ作ってみることにしました。
むかーしArduino Unoでそれっぽいことをしようとしたことはあったんですが、改めてのチャレンジですね。

とりあえず購入したのは、一番ミニマムなデバイスであるATOM Lite、5x5のLEDマトリクス付きのATOM Matrix、そしてGrove規格で接続できる温度センサーの3つです。

 

マイコン触るのも数年ぶりなので、まずは定番のLチカをやって勘を取り戻すところからスタートします。使うのはATOM Matrixの方。

Arduino IDEのセットアップなどは、オフィシャルドキュメントの

docs.m5stack.com

を参考にすればいいので省略。

早速サンプルコードを書き込んでみよう、ということで、Arduino IDEのFile→Examples→M5Atom→Basics→Buttonを選んで出てくるコードを書き込んでみたのですがー。

なんかLEDの表示が欠けてるんですが…?

調べたところ、こちらのページ

pointofviewpoint.linclip.com

曰く、事前にsetWidthHeightをコールしてやる必要があるとのこと。

ということで、サンプルコードのstfBuff関数を以下のように修正。

void setBuff(uint8_t Rdata, uint8_t Gdata,
             uint8_t Bdata) {
    M5.dis.setWidthHeight(5, 5);
    DisBuff[0] = 0x05;
    DisBuff[1] = 0x05;
    for (int i = 0; i < 25; i++) {
        DisBuff[2 + i * 3 + 0] = Rdata;
        DisBuff[2 + i * 3 + 1] = Gdata;
        DisBuff[2 + i * 3 + 2] = Bdata;
    }
}

これを改めて書き込んでやるとー、

はい、全面表示されるようになりました。

 

ただチカチカさせるだけつまらん、ということで、今回は1秒ごとに明滅を繰り返すようなコードを書いてみました。

setBrightnessをコールしたあとでdisplaybuffを呼ばないと、LEDの明るさ変更が反映されないんですね…。

#include "M5Atom.h"

uint8_t
    DisBuff[2 + 5 * 5 * 3];

void setBuff(uint8_t Rdata, uint8_t Gdata,
             uint8_t Bdata) {
    M5.dis.setWidthHeight(5, 5);
    DisBuff[0] = 0x05;
    DisBuff[1] = 0x05;
    for (int i = 0; i < 25; i++) {
        DisBuff[2 + i * 3 + 0] = Rdata;
        DisBuff[2 + i * 3 + 1] = Gdata;
        DisBuff[2 + i * 3 + 2] = Bdata;
    }
}

void setup() {
    M5.begin(true, false, true);
    delay(10);
    setBuff(0x42, 0x8a, 0xf5);
    M5.dis.setBrightness(0);
    M5.dis.displaybuff(DisBuff);
}

void loop() {
  for(int i=0;i<100;i++){
    M5.dis.setBrightness(i+1);
    M5.dis.displaybuff(DisBuff);
    delay(5);
  }
  for(int i=100;i>0;i--){
    M5.dis.setBrightness(i+1);
    M5.dis.displaybuff(DisBuff);
    delay(5);
  }
}

こいつを書き込んだ結果がこちら。上手い具合に行きました。

くらしTEPCOのページから電力使用量情報を取得するツールを作った

東京電力の電力プラン契約を、スマートメーターを使うタイプに変更すると、くらしTEPCOのサイトで使用電力量のデータを見ることが出来るようになります。
最小30分刻みで見ることができ、CSVエクスポート機能もあったりするのですが、どうせならAPIとかで取得して監視ツールに突っ込んでじまえでグラフ化したい! となるわけです。

が、残念なことにデータ取得用のAPIなどは用意されていないので、じゃあ自前で取得する方法を考えないとですねー、ということで、ツールを作りました。 github.com

使い方はREADMEに書いているとおりです。他のツールと併用することを考えているので、基本的には指定された日付のデータしか取得しません(期間指定でどうこう、とかは今の所出来ない)。
ただし他のツールに流し込みやすいように、CSVのデータをそのまま出力するだけでなく、JSONに変換して出力するオプションも着けました。フォーマットはこれでいいのか今一自信がありません…が、まぁとりあえず問題はないかなと。

今回このツールを作るにあたり初めてPythonに触れたわけですが、案外使いやすいですね…。今後しばらくツール類はPythonで書いてみようかしら。

systemd の Service 起動時・停止時にメール通知を送る

前回のエントリーの続き。

Service の自動再起動設定がされている状態で、サービスの再起動が発生した場合にメール通知が飛ぶとうれしいなぁ、という事で調べてみると、同じ事を考えている人が既にいて、こんな感じで実現しています。 qiita.com

が、Apache 2.4 の場合、SIGKILL を送って強制終了した場合、Service のステータスとしては正常終了になってしまうので、OnFailure に書かれたユニットが起動してくれない、という問題にぶち当たりました。

それじゃあいっそ「Service 起動時・停止時に必ずメール通知を送る」ようにしてやろうじゃないか、というのが今回のエントリーの内容です。

通知用のテンプレートユニットを作る

先に、起動時通知用・停止時通知用のテンプレートユニットを作ります。

/etc/systemd/system/service-start-notify@.service

[Unit]
Description = service start notification for %I

[Service]
Type = oneshot
ExecStart = /bin/sh -c 'systemctl status --full "$1" | mail -s "[%H] $1 start" notice' -- %i
User = nobody
Group = systemd-journal

/etc/systemd/system/service-stop-notify@.service

[Unit]
Description = service stop notification for %I

[Service]
Type = oneshot
ExecStart = /bin/sh -c 'systemctl status --full "$1" | mail -s "[%H] $1 stop" notice' -- %i
User = nobody
Group = systemd-journal

mailコマンドで指定している宛先は、通知したいメールアドレスをべた書きしても良いですし、alias を指定しても大丈夫。今回は notice という名前でエイリアスを作って、そこに送るようにしています。

ファイルを作ったら、リロードして有効化します。

$ sudo systemctl daemon-reload

Service の設定を修正する

続いて、Apache2 の Service 設定を修正します。

sudo systemctl edit apache2

でエディタを起動して、以下の内容を追記します。

[Service]
ExecStartPost=/bin/sh -c "systemctl start service-start-notify@$1.service" -- %n
ExecStopPost=/bin/sh -c "systemctl start service-stop-notify@$1.service" -- %n

名前でなんとなく想像出来ると思いますが、ExecStartPost がサービス起動後に実行されるコマンド、ExecStopPost がサービス終了後に起動されるコマンドです。ExecStopPost は正常停止・異常停止のいずれの場合でも実行されるので、Service が止まった場合は必ずメール通知が飛ぶ仕掛けになっています。

この状態で Apache のプロセスを殺すと、notice のエイリアスで指定したメールアドレス宛に、サービス終了通知とサービス開始通知のメールが届きます。

Ubuntu 16.04 LTS で、Apache 2.4 の自動再起動

そもそも最初から自動で再起動するように設定して置いて欲しいんですけどぉ。

Ubuntu 16.04 LTS で入る Apache 2.4 は、ちゃんと systemd 管理になってはいるのですが、自動再起動の設定がされていません。

$ sudo systemctl show apache2
Type=forking
Restart=no <- Restart がデフォルト値のまま!
NotifyAccess=none
RestartUSec=100ms
TimeoutStartUSec=5min
TimeoutStopUSec=5min
RuntimeMaxUSec=infinity
WatchdogUSec=0
WatchdogTimestamp=Thu 2017-03-02 18:10:17 JST
WatchdogTimestampMonotonic=169883017858
FailureAction=none
(snip)

じゃあどうすれば良いかというと簡単で、自分で自動再起動設定を override すれば大丈夫。

$ sudo systemctl edit apache2

とするとエディタが開くので、以下の内容を書き込んでやれば、service の設定が更新されて、自動再起動が有効になります。

[Service]
Restart=always

on-failure にするか always にするかはお好み次第(on-failure だと正常終了した場合は再起動しない)ですが、今回は「どうせ Web サーバなんだし起動するのが当たり前なので always にしとけ」ということで always に。要らなければ service を無効化すれば良いだけですしね。

WWW::Mechanizeで携帯向けサイトを弄れない時はバージョン1.71以上に上げよう(もしくは自分でどうにかしよう)

T/O としてもいい記事ですが、まぁ一応説明を。

PerlのWWW::Mechanizeでサイトを弄る場合で、特に携帯電話(いわゆるガラケー)向けのサイトを弄る場合に、

  • フォームはきちんとHTML内に存在するのにsibmit_form()できない
  • title()でタイトルが取れない

とかいう不思議現象に遭遇することがあります。

これは、WWW::Mechanizeのis_html()メソッドが、Content-Typeとしてtext/html以外が返ってきた場合にはドキュメントをHTMLと見なさず、HTMLとしての解析を行わない事が原因です。携帯電話向けのサイトではContent-Typeとしてapplication/xhtml+xmlを返す事が有るため、この仕様にバッチリとハマり、前述の問題が起こるわけです。 本来xhtmlなドキュメントの場合はtext/htmlではなくapplication/xhtml+xmlを返すのが正しいので、普通のPC向けサイトでもこの事は起こりえるのですが、携帯向けサイトはContent-Typeを厳密に設定する傾向があり(でないと携帯電話が解釈してくれない)、この問題に遭遇しやすいようです。ってか、一般のPC向けサイトで、今日日xhtmlを正しく運用してる所をほとんど見た事がないですね…。

で、解決方法としては、

  • WWW::Mechanizeの1.71でContent-Typeがapplication/xhtml+xmlでもhtmlと見なすように修正が入ったので、これ以降のバージョンにWWW::Mechanizeをバージョンアップする
  • あるいは、自分でWWW::Mechanizeのis_htmlメソッドを上書きする

のどちらかとなります。私は最初新しいバージョンで直っている事に気付かず後者でやりましたが、まあ出来るだけ前者で対応した方が良いと思います。「モジュール内のメソッドをモジュールの外から上書きする」というのはとても黒魔術チックなので…。

配列の差分を取る

たとえば、

my @array_a = (1,2,3,4,5);
my @array_b = (3,4,5,6,7);

とあって、array_aには有るけどarray_bには無い要素だけを取り出したい、というケース。正式には、集合array_aから集合array_bを引いた差集合、とかいう言い方になりますかね。 これを求めるには、以下のようにするとできます。

my @array_c = grep { my $t=$_; ! grep /^$t$/,@array_b; } @array_a;

こうすると、

$ cat tmp.pl
#!/usr/bin/perl
use strict;
use warnings;
my @array_a = (1,2,3,4,5);
my @array_b = (3,4,5,6,7);
my @array_c = grep { my $t=$_; ! grep /^$t$/,@array_b; } @array_a;
print "@array_c\n";
$ perl tmp.pl
1 2

とこんな感じで差分を取ることができます。

同じ理屈で、

my @array_c = grep { my $t=$_; ! grep /^$t$/,@array_b; } @array_a;

こうすれば、両方の配列で重複している要素を取り出すことができます。