2019年11月13日

ESP-WROOM-02(ESP8266)で乾電池駆動の温湿度計を作ってみた

Raspberry PiからArduinoへ

ここ最近IoTがAIと共に「幻滅期」に入ったというニュースが出ていましたが、数年前に発売されたRaspberry Pi(ラズパイ)は今年で4代目が発売され、今やちょっとしたPCと同じぐらいのスペックになりつつあります。

そんなラズパイだとLinuxが普通に動くし、ちょっとした電子工作もできるので自分みたいなソフトウェア側の人間からすととても居心地のよい環境なのですが、本格的なIoTをやりたいと思うとちょっと物足りなく感じるのも正直なところです。

そんなところに「ワンコイン(500円)でインターネットにつながる機器が出た」というニュースを見て衝動買いしたのがESP-WROOM-02(ESP8266)でした。
これはArduiono(アルデュイーノ)というラズパイの先輩?にあたる電子工作の統合開発環境を使って開発ができるマイコンで、ワンコイン(実際には数百円ほど)という安さなのに、WiFi接続や各種I/Oポートも持っているというものです。
(しかも国内で電波を発信できる「技適」も取得している)

前々からArduinoは気になっていたものの、ラズパイからすると敷居が高そうで二の足を踏んでいたところに、安価に開発できると聞いて衝動買い、、したのが4年前くらいになります(それまで寝かせてたw)。

部品調達

ESP8266は既にWiFiに加えてBluetoothにも対応した後継機(ESP32)も出ており、このままだと肥やしになりかねないので、とりあえずまずはATコマンドを試してみようと部品を揃えるところから始めました。

  • PCとESP8266とシリアル通信するため、ebay.comでFT232RLを購入
  • Lチカ用にLEDは対応電圧が分からなかったので適当(2.1V, 50mA)なやつを購入
  • 単3リチウム電池4本で駆動させるため、リード線付き電池ボックスを購入
  • microUSBから電源を取る(+念のため2.1mmDCジャックの)DIPキットを購入
  • 上記電源をESP8266に流すため、3.3V0.5Aの三端子レギュレーターを購入
  • Arduino IDEで作ったソフトを書き込むモードと実行するモードの切り替え、およびESP8266本体再起動のため、切り替えスイッチとリセットスイッチを購入
  • ブレッドボードと各種パーツを接続用のジャンパーケーブルを購入

部品が揃ったら、ググりながら(笑)見よう見まねでESP8266の各端子をブレッドボード上で接続し、最後にmicroUSBを接続して電源を供給したところ、、無事Arduino IDEのシリアルコンソールに文字が出てきました!
(ただ、ファームウェアのアップデートはなぜかChip sync error: Failed to connect to ESP8266: Timed out waiting for packet headerエラーが出て失敗。。)

プログラムの書き込みと実行

次は自作プログラムをESP8266上で動かすのを試してみます。IoT界の「Hello World」はLチカ(LEDをチカチカ点滅させる)らしいので、極性に注意しながらブレッドボードに指し、こちらのプログラムを参考にさせて頂き、WiFi接続とURLアクセスでLEDが点滅するのを確認できました。

温湿度センサー(BME280)から値を取得

自作プログラムの書き込みと実行が確認できたところで、今度はBME280から値を取得してシリアルコンソールに出力するプログラムをこちらのライブラリーを参考にして書き、無事出力できることを確認できました。
ネット上だとBME280から値を取る場合はI2Cを使うことが多いみたいですが、手持ちのBME280はSPIで取得する必要があったため、前述のライブラリーを使用しています。

ディープスリープと電池駆動

ネットワーク接続と値取得が確認できたところで、いよいよこれらを組み合わせて温湿度計プログラムを作ります。ESP8266にはディープスリープ(Deep Sleep)と呼ばれるモードがあり、このモードに入るとCPUやWiFiを全て止めて次の時間まで待機する(リセットがかかる)代わりに、その間ごく微量の消費電力で駆動させることができます。

温湿度の計測は1時間に一度行い、その結果をクラウド上のInfluxDBにPOSTしてまた1時間ディープスリープする形にしました。
動作確認できたところで、最後はmicroUSB電源から電池ボックスのリード線からの電源に切り替えて変わらず動いたら成功です。

#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h> 
ADC_MODE(ADC_VCC); 
#include <SPI.h>
#include "BME280SpiSw.h" 
#define SERIAL_BAUD 115200
#define CSEL_PIN 5
#define MOSI_PIN 13
#define MISO_PIN 12
#define SCLK_PIN 14 
BME280SpiSw::Settings settings(CSEL_PIN, MOSI_PIN, MISO_PIN, SCLK_PIN);
BME280SpiSw bme(settings); 
const char WLAN_SSID[] = "wifi-ssid";
const char WLAN_PASS[] = "wifi-password"; 
const IPAddress addr(192, 168, 0, 251);
const IPAddress gwip(192, 168, 0, 1);
const IPAddress mask(255, 255, 255, 0); 
const char TSDB_HOST[] = "influxdb.example.com";
const int  TSDB_PORT   = 8086;
const char TSDB_PATH[] = "/write?db=weather";
const char TSDB_USER[] = "user";
const char TSDB_PASS[] = "password"; 
void setup() {
  Serial.begin(SERIAL_BAUD);
  Serial.println(); 
  while (!bme.begin()) {
    Serial.println("Could not find BME280 sensor!");
    delay(1000);
  } 
  WiFi.config(addr, gwip, mask, gwip);
  if (WiFi.SSID() != WLAN_SSID) {
    WiFi.persistent(true);
    WiFi.mode(WIFI_STA);
    WiFi.setAutoConnect(true);
    WiFi.begin(WLAN_SSID, WLAN_PASS);
  }
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
void loop() {
  int vcc = ESP.getVcc();
  float temp(NAN), hum(NAN), pres(NAN);
  BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
  BME280::PresUnit presUnit(BME280::PresUnit_hPa);
  bme.read(pres, temp, hum, tempUnit, presUnit); 
  String payload = "temperature,locate=room value=" + String(temp) + "\n";
  payload += "pressure,locate=room value=" + String(pres) + "\n";
  payload += "humidity,locate=room value=" + String(hum) + "\n";
  payload += "vcc,locate=room value=" + String(vcc) + "\n";
  Serial.println("payload=" + payload);

  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    http.begin(client, TSDB_HOST, TSDB_PORT, TSDB_PATH);
    http.setAuthorization(TSDB_USER, TSDB_PASS);
    int ret = http.POST(payload);
    http.writeToStream(&Serial);
    http.end();
    Serial.println("ret=" + String(ret)); 
    WiFi.persistent(false);
    WiFi.disconnect();
  } 
  // enter deepsleep for 3600 seconds
  ESP.deepSleep(3600 * 1000 * 1000, WAKE_RF_DEFAULT);
  delay(1000);
}

まだまだこれから。。

「やっとできた!」と嬉々としてこの記事を書いていたものの、1時間経過してもInfluxDBにはログが記録されていなかったので、上記プログラムか回路側の方にまだ問題があるようです、、でも乾電池で駆動するのは置き場所の制約が緩和されるので、今後の電子工作のアイデアも広がりそうな可能性を感じました。

追記:WiFi.persistent()/WiFi.setAutoConnect()は動いていなさそうだったので削除して都度新規接続するようにし、ディープスリープの3600秒(1時間)を1200秒(20分)に修正ました。

2019年7月29日

Giga LANDISK(HDL-G)を復旧する

はじめに

14年前位に購入したアイ・オー・データ機器のGiga LANDISKというNAS製品があったのですが、ある日HDDが調子悪くなり、中のデータだけPCに退避してしばらく放置していました。

これより高性能なラズパイがある今、復旧させる必要性もあまり感じなかったのですが、当時バッファローの玄箱に代表される組み込みLinuxの学習環境として愛着が多少はあったので、残りの余生をデータ退避先として過ごしてもらおうと復旧作業を行ってみた次第です。

準備

復旧手順は、
  1. 新しいHDDと交換する
  2. Linuxをインストールする
だけなんですが、この製品のHDDは今主流のSerialATAではなくパラレルATAのため、新品での入手はほぼ絶望的で、今回は仕方なくヤフオクで中古を仕入れました。

次のLinuxにインストールについても、時間が経ち過ぎてネット上の情報が少なくなっているのと、この製品をサポートしたLinuxカーネル(Debian)は5世代前のwheezyで、LTSも去年の5月で期限を迎えていました。
(しかも次のバージョンのjessieではサポート自体切られている。。)

HDDにインストーラーを入れる

いくつか不安要素はあったものの、ダメならその時は寿命と諦めるという事で、まずはHDDにDebianのインストーラーを入れるため、母艦PC(Windows)上のHyper-VにUbuntuをセットアップします。

その際、パラレルATA-USB変換ケーブルでHDDと母艦PCを繋ぎますが、デフォルトでは母艦PC側にHDDがマウントされてHyper-V側に接続できないため、ディスクの管理よりHDDをオフラインにしておく必要があります。

オフラインにしたHDDを無事Hyper-V側にマウントできたら、Ubuntuのターミナル上でHDDのセットアップを行います。
# マウントされたHDDデバイスパスを確認
$ sudo fdisk -l
(/dev/sdbなどに表示を確認) 
# インストーラーを入れるパーティション100MBを確保
$ sudo fdisk /dev/sdb
d で既存パーティションを削除
n で新規パーティションを作成
開始シリンダ数は2048でそのまま、次のサイズ指定で+100Mを入力して100MB分のパーティションを作成
p で作成されたパーティションを確認
w で結果をHDDに書き込み 
# 作成したパーティションをext2でフォーマット
$ sudo mkfs -t ext2 /dev/sdb1 
# HDDに読み書きできるようマウント
$ sudo mount /dev/sdb1 /mnt 
# インストーラーをダウンロード
$ wget http://ftp.riken.jp/Linux/debian/debian-archive/debian/dists/wheezy/main/installer-armel/current/images/iop32x/network-console/glantank/initrd.gz
$ wget http://ftp.riken.jp/Linux/debian/debian-archive/debian/dists/wheezy/main/installer-armel/current/images/iop32x/network-console/glantank/preseed.cfg
$ wget http://ftp.riken.jp/Linux/debian/debian-archive/debian/dists/wheezy/main/installer-armel/current/images/iop32x/network-console/glantank/zImage 
# インストーラーをHDDにコピー(initrd.gzは.gzを外してリネーム)
$ sudo initrd.gz /mnt/initrd
$ sudo preseed.cfg /mnt/preseed.cfg
$ sudo zImage /mnt/zImage 
# initrdとzImageに実行権限を付与
$ sudo 0777 /mnt/initrd
$ sudo 0777 /mnt/zImage 
# HDDをアンマウント
$ sudo umount /mnt
以上の作業が完了したら、母艦PCからHDDを取り外し、Giga LANDISKに取り付けます。
この後、最初は機器上のUARTからシリアルコンソール経由でインストール作業を行っていたのですが、時間が経つと応答が返ってこなくなってしまったため通常通りSSH接続にて行いました。
(なお、UART端子はこちらの情報を参考に、▼マークから3.3V、RxD、TxD、GNDの順となっているため、それに従ってUSB-シリアル変換ケーブルとシリアルコンソールを115200/8-N-1に設定して接続します)

ミラーサイトをdebian-archiveにする

Giga LANDISKに有線LANケーブルを接続し電源を入れて数分待つと、DHCPによってIPアドレスが振られてSSHでアクセスできるようになります。
(ユーザー名はinstaller、パスワードはinstall)

詳しい画面はこちらのサイトが参考になりますが、ミラーサイトの選択については前述の通りwheezyはサポート切れのため、一般的なミラーサイトを選択してもエラーが出て次へ進めません。

そのため、Choose a mirror of the Debian archiveで"enter information manually"を選択し、
  • Debian archive mirror hostname に ftp.riken.jp
  • Debian archive mirror directory に /Linux/debian/debian-archive/debian
を入力してミラーサイトを手動で設定する必要があります。
(ftp.riken.jpで決め打ちするより、cdn.debian.orgなどCDNを設定した方がいいかも)

以降は案内に従ってインストールを進めると、無事Debianがセットアップされます。

その他

WiFiルーターの有線LANポート節約のため、USB-Ethernet接続で内蔵イーサネットポートが空いているラズパイにGiga LANDISKを接続しています。そのため、ラスパイ側と母艦PC側で静的ルーティングの設定を行います。
# Giga LANDISK側のIPアドレスを設定
$ sudo vim /etc/network/interfaces
auto eth0
iface eth0 inet static
  address 10.0.3.2
  netmask 255.255.255.0
  gateway 10.0.3.1
  dns-nameservers 10.0.3.1 
# ラズパイのIPフォワーディングを有効にする
$ sudo vim /etc/sysctl.conf
net.ipv4.ip_forward=1 
# 内蔵イーサネットポートにIPアドレスを設定
$ sudo vim /etc/dhcpcd.conf
interface eth0
static ip_address=10.0.3.1/24
static routers=10.0.0.1
static domain_name_servers=10.0.0.1 
# IPマスカレードを設定
$ sudo vim /etc/rc.local
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE 
# 母艦PC側でルーティングテーブルを追加
> route -p add 10.0.3.0 mask 255.255.255.0 10.0.0.3

おわりに

aptコマンドじゃなくてaptitudeコマンドだったり、sambaがSMBv1のため最新のWindowsから接続できないなど細々と問題はありますが、それを解決していくのも組み込みLinuxの醍醐味だったりします。
(自分だけ?)

2019年6月30日

ScalaMatsuri 2019に行ってきた


前回の投稿が「ScalaMatsuri 2018に行ってきた」で、その間何も投稿していなかったのかと軽くショックなのですが、今年は初日が平日開催だったのでお休みを頂いてお邪魔してきました。

1日目

Scalaライブラリを作る前に知っておきたいメンテナンスのこと
ライブラリを提供するにあたって、どうやったら使ってもらいやすいか(特にJavaとの相互運用性)や、リリースバージョン間の互換性や依存関係など、普段のサービス開発とは異なった視点が必要なところは面白そうに感じました。

Scalaにおける型クラス入門
”型クラス≒型システムにより支援されたストラテジパターン”という切り口が個人的にしっくりきたのと、Scala2のimplicitとScala3からのdelegate-forに書き方が変わるという情報が収穫でした。

コードで理解するPlayFrameworkの脆弱性
PlayFrameworkで過去に報告された脆弱性を例に、何が原因でどう対策したのかの解説が行われたのですが、どれも何気に普段の開発でも踏みやすいような内容だったので、後からじわじわと空恐ろしくなりました。。

ScalaプロジェクトでEffを使用する利点
Futureとfor式を使ってScalaっぽく書きたい(けど挫折した)身としては、より便利とおすすめされるEffはとても気になりました。

こんなに違うScalaとKotlin
ScalaとKotlinの構文的やリストの挙動の違いを見て、Scalaと構文が似てる気持ちでKotlinに入門すると痛い目を見そうな感じがしました。

DOT計算をやさしく説明する
不勉強でうまく感想を書くことができませんが(汗)、プログラミング言語が満たす性質を証明する何か(チューリング完全?)のような印象を受けました。

ハイパフォーマンスScala
ExecutionContextやリストの特性、Boxing/Unboxingの挙動に気を使うことでより高いパフォーマンスを出すコードを書くためのコツを知ることができました。計測大事。

Scala💛Graal
個人的に最近気になっているGraalについてTwitter社での取り組みについての発表でしたが、GraalVMとGraalは異なるもの(後者はJavaでのJIT実装)で、JVM標準のJITよりモダンなコードで書かれたGraalをチューニングすることでパフォーマンス改善を行うための試行錯誤の過程が面白かったです。

継続とDI
オブジェクト指向言語でよく行われている委譲とDIに対し、関数型言語では”継続”というアプローチを取ることでアドホックに合成がしやすいなど、両言語の特性を使えるScalaならではの使いどころなどが説明されました。

2日目

仕事でScalaを使おう - Arm Tresure DataでのAirframe活用事例
Scalaのロギングライブラリを探していた際、たまたま見つけたのがairframe-logで、そこからちょっと気になっていたので今回いい機会となりました。

いかにして我々は10年もののPerlプロダクトをScalaでリプレースしたか
”レガシーシステムをScalaでリプレースする”は鬼門のように感じていた時期があったのですが、こうやって着々と完走した事例が紹介されるのを見ると、必要なのはやりきる力なんだと改めて思いました。

over40の転職成功事例(オフレコ枠?)
今いらっしゃる職場に至るまでの経緯(しくじり)とその対処法、組織での振る舞いや心構えなどの発表と議論が行われました。

ScalaでWeb開発するときのデファクトな構成が知りたい
Webフレームワーク、DBライブラリ、JSONライブラリのそれぞれで、参加者の方が業務でよく使われているプロダクトの多数決とそのいいところの共有が行われました。

gRPC gateway with Scala
セッション後の質疑応答の中で、実務としてのマイクロサービスでの開発や運用が普通に語られているのが印象的でした。

From Go To Scala Easy vs Simple
Go言語とScala(とKotlin)の"Simple"と"Easy"の感じ方の違いや、C++やSwiftも参戦しての熱い議論(プロレス)が行われました。みんなちがってみんないい。

Finagle(thrift) admin-page probrem
Finagleでやり取りされるthriftのraw dataが格納されたMySQL(KVSとして有名!)の管理ツール画面フォームを、thriftのコンパイラが生成するASTより生成する発表が行われました。

おわりに

Scalaを触る時間が少しずつ増えてきたせいか、アカデミックな内容からより実務的な内容を選択したような気がしました。implicitは怖さが少し軽減されてきたので、次は既存の課題をScalaらしい関心の分離が行えるような取り組みを行っていきたいと思います。