勝手な電子工作・・

勝手なオリジナル電子工作に関する記事を書きます

室内空気の微小粒子状物質濃度記録計ーはんだ付けがこんなに(+_+)?

小さなPM2.5記録計を作りました。最近、仕事に関連した製作が多いため、工作室の空気の汚れが気になっています。とくにオリジナル基板のほとんどはCNCルーターで削るため、細かい塵がさぞやたくさん出ているかなと。削るときはもちろろんN95マスクをつけ窓を開けて換気していますが。

この機械をブレッドボードで試作してすぐに測ってみると、外気の値は「リアルタイムPM2.5マップ」の当地の値とよく一致。その日は花粉が多くとんでいる日でした。そして気になっていた測定を。。。ところが、CNCで基板を切削する際には、想像していたほどは室内の微小粒子状物質が増えないのがわかりました。部屋のよごれからみて、意外に大きな埃だけを出すようです。その代わり、電子工作では他の作業がもっとひどく汚すことが間もなくわかり、驚きました!

PM2.5センサーのうち、小型で手ごろなのはレーザー散乱光計測方式のものです。ミニファンで外部の空気を吸い込み続け、内部のダクトを通る際に前方特定角度にあるレーザーからの散乱光の強さを測定することで、通過する微小物質の個々の大きさを識別するしかけになっています。

ここではWinsen社のZH03Bを使いましたがArduinoで至極楽ちんに短時間でできました。このセンサーでの測定データを得るのには複数のプロトコル(いずれもUART)が用意されています。その中で、センサー側から一定間隔で一方的に送ってくるデータを素直に受け取る方式、Sensor Initiative Uploadが一番楽です。その場合に必要な接続はセンサー側のTxラインとGndだけです。

接続はごく簡単ですので、この記事の最後につけるArduinoスケッチを参照してください。ロガーとするために必要なデバイスは、他に①リアルタイムクロック(I2C接続)と ②マイクロSDカードモジュール(SPI接続)をつなぎ、またPM2.5の数値をリアルタイム表示するための③4桁LED(TM1637、2線接続)も接続します。これだけ使っても、速度の上でも容量の上でもUnoで十分な余裕があります。

ZH03Bは立てて設置する必要がありますが、それだけで50mm強の高さがあります。そこで、この際はRTCモジュールもSDカードモジュールもピンの着いたまま立てて、メスソケットに挿しこむことにしました。こうすると基板の作成もとても楽。

え?ランドがないうえ、角が多いひどい基板だ?--> これでいいんです^^;  それを言うとブレッドボードはこの何十倍も問題。そんな環境で動くデジタルものは何ら問題ないです、アナログでも高周波でもありませんからね。そういうものは全てCNCルーターの高価な超硬エンドミル寿命を優先、つまり削る個所を極端に減らすパターンにしてるわけです。

センサーの空気の流入口と吸出口をふさいではいけないので、前後を開放。アクリル板を加工した簡単な屋根を付けることにしました。マイクロSDカードを取り出す際は屋根を倒して開ける構造にすることで、幅76x奥行60x高さ60のコンパクトな機械になりました。

なお、2号機ではスイッチを1つ追加しましたが、これはマイクロSDへの物理的書き込み中に電源オフが重ならないようにするためのもの(重なる確率はおよそ数十万分の1ですが)なので、なくても問題ないかとは思いますが。とにかく長時間記録したものを絶対失いたくないので^^; 電源を切るときだけこのスイッチをオンにするわけです。この記事の最後につけるプログラムはそのスイッチもつけたものです。無視されても良いかと思います。

この機械は忙しいさなかに考えながら少しずつの時間を使って作りましたが、めでたく2台が完成!


右が1号機、左が2号機でスイッチが1つ追加され、高さ、幅ともに5mmほど小さくできました。2台作ったのはもちろん別々に測ったデータがどれほど一致するかを見たかったからです。

そして2号機の製作中に1号機で記録をつづけていたら、ビックリすることがわかりました。

この日は花粉もなく外気は綺麗な日です。なんとはんだ付けで微小粒子状物質が大量にまき散らされるのがわかりました。たしかに松脂の煙を含む蒸気をだしてますし臭います。

しかし数メートルはなれたこのセンサーの位置(窓に近い)で、こんなに直ちにばっちり記録されるとは驚き・・・。

そして2号機が完成したので、同時に測定を開始。その結果2台の数値は常に非常によく一致することがわかりました!

別個の独立した測定装置でこんなによく一致するとは!、これもまた今回の製作の驚きでした。

今回は簡単な記事で図面等も省略させていただきますが、最後にプログラムをつけておきます。これをご覧いただいた方が接続などはわかりやすいかもしれません。

/***************************************************************
   Air-dust Density Logger for Arduino-Uno (ATmega328P)
      Initial Version V.00     Mar.26 2022    (c)Akira Tominaga
      Functions:
        - Measure PM1.0, PM2.5 and PM10 densities (μg/m^3).
        - Display current density of PM2.5  to TM1637 LED.
        - Record each minuite's average data to micro SD card.
      Pin connections:
              Air-dust sensor ZH03B; TX = software Rx pin 2.
              Micro SD card; MOSI=11, MISO=12, CLK=13, CS=10.
              Real time clock DS3231; SDA=A4, SCL=A5.
              TM1637 4digit LED display: DIO=6, CLK=7.
              Optional switch: pin9 to avoid pwr-off during
              SD-write timing (though extremely rare).
 ***************************************************************/
#include "SoftwareSerial.h"
#define sRx 2                     // sS-Rx = sensor Tx pin
#define sTx -1                    // sS-Tx = sensor Rx not connected
#define sSbaud 9600               // software-serial baud rate
#include "Wire.h"                 // for RTC interface
#include "SPI.h"                  // for SD interface
#include "SD.h"                   // for micro SD drive
byte rBt[24];                     // rcv bytes from software-Serial
// *** for air-dust sensor ***
SoftwareSerial sS(sRx, sTx);      // sS as SW-serial symbol
uint16_t PM1_0, PM2_5, PM10_;     // measured values
#define iuLen 24                  // length of sensor-Initiative-Upload
uint16_t cSum;                    // checksum
uint8_t dC = 0;                   // data counter to calculate averages
uint16_t tPM1_0 = 0;              // total PM1.0 value
uint16_t tPM2_5 = 0;              // total PM2.5 value
uint16_t tPM10_ = 0;              // total PM10. value
// *** for Real Time Clock ***
#define RTC_addr 0x68
#define mdI 0                     // Initial mode
#define mds 1                     // ss
#define mdm 2                     // mm
#define mdh 3                     // hh
#define mdW 4                     // WW=Day of Week
#define mdD 5                     // DD
#define mdM 6                     // MM
#define mdY 7                     // YY
char  MMDD_hhmm[14];              // editing area for calendar and clock
char  hhmmss[10];                 // editing area for hh:mm:ss
uint8_t vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0}; // integers for RTC data
// *** for micro SD drive ***
#define cS 10                     // chip select
String fName;                     // CSV file name
String Rec;                       // contents of record
uint8_t minSave;                  // save area for minute value
// *** SD disable switch to avoid power-off during SD writing
#define SDsw 9                    // SD enable/disable switch pin
// *** for TM1637 display to diplay real time PM2.5 μg/m^3
#include "TM1637Display.h"
#define tmDIO 6                   // TM1637 DIO
#define tmCLK 7                   // TM1637 CLK
TM1637Display TM(tmCLK, tmDIO);  // class TM for TM1637

void setup() { // ***** Arduino setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial sS
  Wire.begin();                   // start I2C
  // *** start TM1337 display
  TM.setBrightness(2); // *** set LED brightness(low0-high7)
  const byte segs[] = {0x40, 0x40, 0x40, 0x40}; // "----"
  TM.setSegments(segs, 4, 0);     // displsy "----"
  // *** set SD file name as MMDDhhmm.csv, using start-time
  getTime();
  minSave = vI[mdm];              // save minute value for later use
  sprintf(MMDD_hhmm, "%02d%02d%02d%02d.csv", vI[mdM], vI[mdD], vI[mdh], vI[mdm]);
  fName = String(MMDD_hhmm);
  pinMode(SDsw, INPUT_PULLUP);    // set SD enable/disable Sw HIGH(enable)
  // *** check SD card
  if (!SD.begin(cS)) {
    Serial.println(F("SD err"));
    while (true) {}
  }
  File aqLog = SD.open(fName, FILE_WRITE); // make the file for data
  // ***** write column header
  aqLog.println("MM/DD-hh:mm,PM1.0,PM2.5, PM10");
  Serial.print(fName); Serial.println(F(" created"));
  aqLog.close();
  delay(200);
}
void loop() {  // ***** Arduino Loop *****
  while (sS.available() < iuLen) {} // wait if not data ready
  // *** read data from sensor
  for (int j = 0; j < iuLen; j++) {
    rBt[j] = sS.read();
  } // got data
  // *** check validity of the data
#define okD 0x00
#define ngD 0x01
  byte ckD = okD;
  // *** confirm correct sensor-initiative-upload
  if (rBt[0] != 0x42) {           // if invalid I.U. header
    ckD = ngD;                    // do not use error data
    flushData();
  }
  // ***** check Checksum *****
  cSum = 0;
  for (uint8_t j = 0; j < iuLen - 2; j++) {
    cSum += rBt[j];
  }
  byte cSh = cSum / 256;
  byte cSl = cSum % 256;
  if ((cSh != rBt[22]) | (cSl != rBt[23])) { // if error,
    Serial.println(F("*Cksum err")); // then
    ckD = ngD;                      // do not use error data
    flushData();
  }
  // *** process for valid data only
  if (ckD == okD) {
    getTime();
    sprintf(MMDD_hhmm, "%02d/%02d-%02d:%02d", vI[mdM], vI[mdD], vI[mdh], vI[mdm], vI[mds]);
    sprintf(hhmmss, "%02d:%02d:%02d ", vI[mdh], vI[mdm], vI[mds]);
    PM1_0 = rBt[10] * 256 + rBt[11];
    PM2_5 = rBt[12] * 256 + rBt[13];
    PM10_ = rBt[14] * 256 + rBt[15];
    Serial.print(hhmmss); Serial.print(PM1_0);
    Serial.print("-"); Serial.print(PM2_5);
    Serial.print("-"); Serial.println(PM10_);
    TM.showNumberDec(PM2_5, false, 4, 0);   // show PM2.5 to LED
    // *** if minute changed record log, else accumulate data
    if ((minSave != vI[mdm]) & (dC > 0)) {  // if minute value changed,
      minSave = vI[mdm];            // save new minute to check
      float fPM1_0 = (float)tPM1_0 / (float)dC;
      float fPM2_5 = (float)tPM2_5 / (float)dC;
      float fPM10_ = (float)tPM10_ / (float)dC;
      Rec = String(MMDD_hhmm) + "," + String(fPM1_0, 2) + "," + String(fPM2_5, 2) + "," + String(fPM10_ , 2);
      putLog();
      Serial.println(Rec);
      dC = 0; tPM1_0 = 0; tPM2_5 = 0; tPM10_ = 0; // reset accumulated values
    } else {                        // if not timing, accumulate totals
      dC++;  tPM1_0 += PM1_0; tPM2_5 += PM2_5; tPM10_ += PM10_;
    }
  }
  // no delay allowed in the sensor-initiative-upload mode
} // end of loop()
/***********************************************************
    User defined functions
 ***********************************************************/
// ***** get time *** getTime() *****
void getTime(void) {
  byte  vR[8];                      // values in RTC registers
  Wire.beginTransmission(RTC_addr);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC_addr, 7);
  while (Wire.available() < 7) {} // wait for data ready
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
  // *** convert RTC-format to Integers
  vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
  vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
  vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  vI[mdW] = vR[mdW];
  vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}
// ***** check and flush if broken data exists
void flushData(void) {
  int resBt = sS.available();
  if (resBt > 0) {              // if rest of data exists
    for (int k = resBt; k <= 0; k--) {
      rBt[0] = sS.read();       // dummy read for flush
    } // end for
    Serial.println(F("Flushed"));
  } // end if
}
// ***** put log Rec *** putLog() *****
void putLog(void) {
  if (digitalRead(SDsw) == HIGH) {    // do only when SDsw is HIGH
    File aqLog = SD.open(fName, FILE_WRITE);
    aqLog.println(Rec);
    aqLog.close();
  }
}
// ***** End of Sketch **

 

以上、短時間でご紹介しましたが、この記事が今後どなたかのお役に立てば幸いです。

 

 

©2022 Akira Tominaga, All rights reserved.

 

かんたん/便利な天気予報表示器(Arduino-IDE, ESP32)

f:id:a-tomi:20220306161436j:plainこの季節は天気予報が頻繁に変わるのに気づきます。PCやスマホなどで見れば豊富に出てくるわけですが、この際は、常時表示してくれる小さな予報器が手元に欲しくなりました^^; 今回はその試作記事を書きます。

筆者はこのところ省エネ型土壌水分(pF)計などに没頭中なのですが、出せる内容の開陳は少し先になりそう。そこで、このブログの記事を長らくさぼってしまってすみませんm(_ _ )m。久々に時間の取れる週末が来たので、気分転換も兼ね、天気予報器作りに早速トライをしました。

自分で気象観測して予報してもあまり当たりません(・・? 。ですからもちろんWebのオープンデータを使います^^; ので、逆にすぐできそうです。ネットを少し調べた限りは、世界の地域とデータを広くカバーするOpenWeatherMapの個人用APIを使うのがよさそうです。常に更新されています。

Weather API - OpenWeatherMap

使い方はここで書くより、https://openweathermap.org/guideを直接見てみていただくのが早くて正確だと思います。丁寧に書かれていますし、ブラウザーの翻訳などで和訳しても読み易い感じですから。

f:id:a-tomi:20220306160802j:plain

上は画像なので赤字のリンクは効きません、リンク先で直接ごらんくださいね。

おおざっぱに言えば、まずeMailアドレスで個人登録をしてAPIキーをリクエスト。しばらく待つと登録したマイアカウントでAPIキーが受け取れます。この予報器では当地の3時間おきの予報を8個、つまり24時間ぶん表示することにしました。

この目的では5 Day / 3 Hour Forecastを使います。データとしては5日分が来ますが、なにせ、これより後の予報はだいぶ変わりやすいよう(??)で。

無料プランのアクセス頻度制限はありますが、その中で一番きつい値は1年間百万回以内。これは1分1回のアクセスなら十分守れる緩い制限ですね。表示器を複数使う場合は取得頻度を減らすとか、あるいは別アカウントで行うかでしょうか。

今回はマイコンにESP32-DevKitを使って試しましたが、あっさりと動きました\(^o^)/。試作段階ではありますが、ここで紹介したいと思います。LCDへの表示は少し複雑なので、まずはシリアルだけ出力する単純なプログラムの例をつけます。

/*****************************************************************
    Local Weather Forecaster  for ESP32 (Serial output)
      Original version V.00    Mar.5, 2022 (c) Akira Tominaga
      Functions:
        - Get forecast Json data via OpenWeatherMap API.
        - Extract required data and print to Serial monitor.
 * ***************************************************************/
#include "HTTPClient.h"
HTTPClient http;
#include "ArduinoJson.h"
// *** for accesses to Weather-Map API
const char* ssid = "Your SSID here"; 			// (1)
const char* password =  "Your password here";   	// (2)
const String API_URL = "http://api.openweathermap.org/data/2.5/";
//const String inq_Weather = "weather?q=Saitama,jp&APPID="; //(3)
const String inq_Fcst = "forecast?q=Saitama,jp&APPID=";     //(4)
const String key = "your API key given by openweathermap";  //(5)

void setup() {  // ***** ESP32 setup() ******
  Serial.begin(9600);  delay(100);
  WiFi.begin(ssid, password);  	// connect to WiFi network
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("Connected to WiFi\n");
}
void loop() { // ***** ESP32 loop() *****
  http.begin(API_URL + inq_Fcst + key); // get weather fcst
  while (http.GET() <= 0) {      // if no response,
    Serial.print("x");           // then print x
    delay(3000);                 // try 3 sec later
  }
  String payload = http.getString();  //get json data
  // Serial.println(payload); 	// for debug only
  DynamicJsonBuffer jBuf;
  String json = payload;
  JsonObject& wD = jBuf.parseObject(json);
  if (!wD.success()) {
    Serial.println("parseObject Err");
  } else {
    // *** select from Json Buffer and show them
    Serial.println("");
    for (int dN = 0; dN < 8; dN++) {
      const char* sky = wD["list"][dN]["weather"][0]["main"].as<char*>();
      const double temp = wD["list"][dN]["main"]["temp"].as();
      const double hum = wD["list"][dN]["main"]["humidity"].as();
      const double pres = wD["list"][dN]["main"]["pressure"].as();
      // *** get Japan time by adding 9hours (i.e.: + 3 in the array)
      const char* dt = wD["list"][dN + 3]["dt_txt"].as<char*>(); // +9h = JST
      // *** replace weather expressions, as you like
      String Weather = String(sky);
      if (sky[2] == 'e') Weather = " Fine "; // Clear
      if (sky[3] == 'u') Weather = "Cloudy"; // Clouds
      if (sky[2] == 'i') Weather = " Rainy"; // Rain
      if (sky[0] == 'S') Weather = " Snowy"; // Snow
      String NN_temp = String(temp - 273.2); // convert absolute temp to Celsius
      NN_temp = NN_temp.substring(0, NN_temp.length() - 1); // cut 0.01 digit
      if (NN_temp.length() == 3) NN_temp = " " + NN_temp;
      // *** shorten calendar expression
      uint8_t iMM = (dt[5] & 0x0F) * 10 + (dt[6] & 0x0F);
      uint8_t iDD = (dt[8] & 0x0F) * 10 + (dt[9] & 0x0F);
      uint8_t ihh = (dt[11] & 0x0F) * 10 + (dt[12] & 0x0F);
      char MDDhh[12];
      sprintf(MDDhh, "%2d/%02d %02d:00", iMM, iDD, ihh);
      // *** output to Serial Interface
      Serial.print(MDDhh); Serial.print("  ");
      Serial.print(Weather);
      Serial.print(" "); Serial.print(NN_temp); Serial.print("℃, ");
      Serial.print(hum, 0); Serial.print("%, ");
      Serial.print(pres, 0); Serial.println(".hPa");
    }
    http.end();               // release resources
    delay(60000);             // ***update every 60S < 530,000/Y < rule/Y
  }
}
// *** end of program

 

上のソース内でコメントの右につけた(1)(2)(5)はご自分の環境に合わせてセットしてください。(3)、(4)には使いたい地点近くでOpenWeatherMapにある都市名等を指定してください。(3)の現在情報は今回使いませんのでコメントにできます。

これで、次のようなアウトプットがシリアルモニターに出ます。


 3/06 12:00  Cloudy 10.9℃, 23%, 1005.hPa
 3/06 15:00  Cloudy 10.8℃, 21%, 1005.hPa
 3/06 18:00  Cloudy  8.5℃, 26%, 1007.hPa
 3/06 21:00  Cloudy  6.0℃, 29%, 1010.hPa
 3/07 00:00   Fine   5.4℃, 30%, 1010.hPa
 3/07 03:00   Fine   5.0℃, 33%, 1012.hPa
 3/07 06:00   Fine   4.6℃, 33%, 1014.hPa
 3/07 09:00  Cloudy  7.5℃, 23%, 1016.hPa

 3/06 12:00  Cloudy 10.9℃, 23%, 1005.hPa
 3/06 15:00  Cloudy 10.8℃, 21%, 1005.hPa
 3/06 18:00  Cloudy  8.5℃, 26%, 1007.hPa
 3/06 21:00  Cloudy  6.0℃, 29%, 1010.hPa
 3/07 00:00   Fine   5.4℃, 30%, 1010.hPa
 3/07 03:00   Fine   5.0℃, 33%, 1012.hPa
 3/07 06:00   Fine   4.6℃, 33%, 1014.hPa
 3/07 09:00  Cloudy  7.5℃, 23%, 1016.hPa

 

APIを用いるこのプログラムではデータをJson形式で取得します。そのためJsonデータをこのプログラムではjBufにいったん収容することになりますが結構長いデータです。そして、”ArduinoJson”ライブラリーを使って、必要なデータのポインターを得ることで、使う内容だけを引っ張り出します。元のJsonデータはネスト構造をしていて分かりにくいため、いったんテキストにして机上でじっくり分解すると間違いを起こしにくいかと思います。

OpenWeatherMapのAPIデータでは、各要素の初めのほうに収容される時刻は”dt”で、UnixTime(1970年1月1日0時0分0秒からの経過秒数)が使われています。それから時刻に変換するのは手間です。そこでよく見ると、データの後ろの方にテキスト表示された”dt_txt”があるので、そちらを使うと楽です。ただし表示は国際標準時ですから、これを日本標準時に変更する必要があり、9時間を加えることになります。

9時間を加えるだけといっても、まともに計算しようとするなら、うるう年の判別などまで必要になってしまいます。ここで考え付いた方法は、常に3つ先(つまり9時間先)の”dt_txt”を使用する方法で、これで時間換算の手間を全て回避しています。

 

さて、次はカラーTFT-LCDに表示をします。デバイスは前に記事で紹介した3.5インチ、480x320ピクセル表示のものです。

https://a-tomi.hatenablog.com/entry/2021/05/24/105516

ESP32ではこのようなグラフィック表示のサポートがかなり安定してきたように思います。TFT-LCDでなくとも何でもよいわけですが、ここではESP32用Arduino-IDEライブラリーとして”TFT_eSPI()”を使っています。ただ、その際にスケッチと同一フォルダーの中に、その一部である“User_Setup.h”、と無料フォント指定をする“Free_Fonts.h”を、自分の環境に更新したものを同居させます(もしそうしない場合、他のデバイスを使うたびにライブラリー内のこれらを変更することになり、既に作ったものを使うときに改めて戻したりする必要がありそうですから)。

f:id:a-tomi:20220306162134j:plain

冒頭の写真のように、ESP32側で使うピン設定は、TFT側ピン配列と同順にしており、なるべくまとまるように設定していますが、WiFiと競合するピンを除けばどのようにでもできます。

こちらのプログラムは次のとおりです。

/*****************************************************************
    Local Weather Forecaster  for ESP32
      Original version V.00    Mar.5, 2022 (c) Akira Tominaga 
      Functions: 
        - Get forecast Json data via OpenWeatherMap API. 
        - Extract required data and display on TFT-LCD.
      LCD (ILI9488 480x320) to ESP32 pins:(reflect to User_setup.h) 
        SCLK-16, MOSI-17, DC-18, RST-19, CS- 21  
 * ***************************************************************/
#include "HTTPClient.h"
HTTPClient http;
#include "ArduinoJson.h"
#include "TFT_eSPI.h"         // with "User_Setup.h"
#include "SPI.h"
#include "Free_Fonts.h"       // Include this into sketch folder
TFT_eSPI tft = TFT_eSPI();

// *** for accesses to Weather-Map API
const char* ssid = "Your SSID here";
const char* password =  "Your password here";
const String API_URL = "http://api.openweathermap.org/data/2.5/";
const String inq_Weather = "weather?q=Saitama,jp&APPID="; // set your city
const String inq_Fcst = "forecast?q=Saitama,jp&APPID=";   // set your city
const String key = "your API key given by openweathermap";

void setup() {  // ***** ESP32 setup() ******
  tft.begin();
  tft.setRotation(1);               // from 320x480 to 480x320
  tft.fillScreen(TFT_BLACK);        // background color
  tft.setTextColor(TFT_WHITE);      // set initial text WHITE
  tft.setTextDatum(TC_DATUM);       // Datum at top center
  header("Weather forecast at Saitama City"); // draw header
  footer("https://a-tomi.hatenablog.com/");   // draw footer
  tft.setTextColor(TFT_GOLD); tft.drawString("by Akira Tominaga", 239,150,4);
  WiFi.begin(ssid, password);       // connect to WiFi network
  while (WiFi.status() != WL_CONNECTED) { // if not connected
    delay(1000);                    // then wait and do again
  }
}
void loop() { // ***** ESP32 loop() *****
  http.begin(API_URL + inq_Fcst + key); // get weather fcst
  while (http.GET() <= 0) {        // if no response,
    delay(3000);                   // then wait and do again
  }
  String payload = http.getString(); //get Json format data
  DynamicJsonBuffer jBuf;
  String json = payload;
  JsonObject& wD = jBuf.parseObject(json);
  if (!wD.success()) {
    //Serial.println("Err at parseObject"); // for debug only
  } else {
    // *** select from Json buffer and show them
    tft.fillRect(0, 29, 480,269, TFT_DARKGREEN); // erase former data
    int xpos =  10, ypos = 40;  // set initial LCD postion
    for (int dN = 0; dN < 8; dN++) {
      const char* sky = wD["list"][dN]["weather"][0]["main"].as<char*>();
      const double temp = wD["list"][dN]["main"]["temp"].as();
      const double hum = wD["list"][dN]["main"]["humidity"].as();
      const double pres = wD["list"][dN]["main"]["pressure"].as();
      // *** get Japan time by adding 9hours (i.e.: + 3 in the array)
      const char* dt = wD["list"][dN + 3]["dt_txt"].as<char*>(); // +9h = JST
      // *** shorten calendar expression to MDDhh
      uint8_t iMM = (dt[5] & 0x0F) * 10 + (dt[6] & 0x0F);
      uint8_t iDD = (dt[8] & 0x0F) * 10 + (dt[9] & 0x0F);
      uint8_t ihh = (dt[11] & 0x0F) * 10 + (dt[12] & 0x0F);
      char MDDhh[12];
      sprintf(MDDhh, "%2d/%02d %02d:00", iMM, iDD, ihh);
      // *** show data on TFT-LCD
      tft.setTextDatum(TL_DATUM);  // Datum at top left
      tft.setTextSize(1); tft.setTextColor(TFT_WHITE);
      String ss = String(MDDhh);  xpos = 10;
      tft.drawString(ss, xpos, ypos, 4); // draw date and time w/Font-4
      // *** replace weather expressions, as you like
      String Weather = String(sky);
      if (sky[2] == 'e') {        // if Clear
        Weather = "  Fine  "; tft.setTextColor(TFT_YELLOW);
      }
      if (sky[3] == 'u') {        // if Clouds
        Weather = "Cloudy"; tft.setTextColor(TFT_LIGHTGREY);
      }
      if (sky[2] == 'i') {        // if Rain
        Weather = " Rainy "; tft.setTextColor(TFT_SKYBLUE);
      }
      if (sky[0] == 'S') {        // if Snow
        Weather = " Snowy "; tft.setTextColor(TFT_SILVER);
      }
      // *** prepare and output strings
      String stemp = String(temp - 273.2); // convert temp-K to Celsius
      stemp = stemp.substring(0, stemp.length() - 1); // cut 0.01 digit
      if (stemp.length() == 3) stemp = "  " + stemp; // fix length
      String shum = String(hum); shum = shum.substring(0, shum.length() - 3);
      String shPa = String(pres); shPa = shPa.substring(0, shPa.length() - 3);
      ss = Weather + stemp + "C, " + shum + "%," + shPa + ".hPa";
      // *** output weather
      xpos = 150;                     // set xpos for weather
      tft.drawString(ss, xpos, ypos, 4);  // Draw text with Font-4
      ypos += 31;
    }
    http.end();               // release resources
    delay(60000);             // ***update every 60S < 530,000/Y < rule/Y
  }
}
/********************************************************************
    User defined functions
 *  *****************************************************************/
// ***** show screen header *** header(string) *****
void header(const char *string) {
  tft.setTextSize(1);
  tft.fillRect(0, 0, 480, 30, TFT_NAVY);
  tft.drawString(string, 239, 2, 4); // Font 4 with default bg
}
// ***** show screen footer *** footer(string) *****
void footer(const char *string) {
  tft.setTextSize(1);
  tft.fillRect(0, 300, 480, 320, TFT_NAVY);
  tft.drawString(string, 239, 300, 2); // Font 2 for fast drawing with background
}
// ***** end of program ***

f:id:a-tomi:20220306160401j:plain

以上、ごく簡単な紹介でしたが、もし皆様の何らかのお役に立てばとても幸いです。

 

©2022 Akira Tominaga, All rights reserved.

 

ロードセルで精密秤(簡単・正確、コスパも抜群)

f:id:a-tomi:20211008171929j:plain

趣味で土壌水分のデジタル負圧測定に取組中ですが、そちらは試作までに期間がかかりそうです。息抜きにストレーンゲージのデジタル測定をしてみました。最近の測定精度やコスパの向上はどんな感じかと・・。

やってみたら、ずいぶん簡単・正確・低コストになったものです!精密な測定が今や簡単、0.1グラム単位の秤などがたやすくできます。以下、これを紹介したいと思います。0.01グラム単位でもできそうです。

昔からあるストレーンゲージですがゲージ自体の改善はもちろん、測定がデジタル化でおおいに進歩。ただ、僅かな伸び縮みを検出する抵抗なので直接扱うとそれなりに手間がかかります。エポキシ等での接着も、細いリードの扱いなども。また場合によっては、熱膨張率によるごく僅かな「見かけの歪」を防ぐため貼付対象に合うゲージ選択(1例:アルミ系なら約24ppm/℃、鉄系なら12ppm/℃)など。伸びる側と縮む側各2か所ずつ計4枚でホイートストーンブリッジにして測ります。

手持ちの1例は次で、7.5 x 4mmほどの大きさ(写真下に1mmの目盛り)で抵抗値は1000Ωでアルミ系材料用。基本型は同じものを4枚使ってフルブリッジ回路にします。基本型用以外に目的に応じパターンが複数あるものや45度のもの、より小さなゲージなどもあります。

f:id:a-tomi:20211008174241j:plain

現代では検出場所の近くでデジタル化し、結果をデータで出力することでノイズの影響などが少なくなっています。HX711はそのための便利なICです。ADCは実に24ビット精度で、それに入力する高精度のアンプを内蔵。ブレークアウトモジュールが次の写真。

f:id:a-tomi:20211008173600j:plain

精密なAD変換やその温度補正など、頼れるこのらくちんなモジュールは、海外ネットでは僅か50円台で売られています!これを使わない手はありませんね。

 

ネットで探すと貼る相手の材料(ロードセル)も数十円からあります。さらに「ストレーンゲージ貼付済のロードセル + HX711モジュール」で、な、な、なんと百数十円で売られています。それは助かるなあ^^ これならすぐできて相当楽でしょう。

f:id:a-tomi:20211008180437j:plain

販売済数評価コメントなどを一応よくみて、信頼できそうな出品元からポチりました。次です。

f:id:a-tomi:20211008180915j:plain

1kg用を求めましたが、今回のテスト結果からみると10kg用などの丈夫なものであっても、測る単位が0.1gなら十分精密に測れそうです。

入手したゲージ貼付済ロードセルは次です。

f:id:a-tomi:20211008181642j:plain

f:id:a-tomi:20211008181822j:plain

中心辺りに双眼鏡型の空洞があり、「ダブルベンディングビーム型ロードセル」とか、「バイノキュラーデュアルビーム型ロードセル」と呼ばれるようです。

ところで、この空洞によって、単なる直方体の単一ビームとは違う伸び縮みをします。力によるひずみを大げさに描けば次の図のようになると思います。手書きですみませんが。

f:id:a-tomi:20211008182531j:plain

下の形だとPに荷重がかかったときに沈む方向はまっすぐ下です。というより、P点付近の天端は傾かず平行移動し、片持ち梁のような梃の原理は働きません。また、荷重の位置が中心線から左右にずれても、測定値が変化しないしくみです。それによって秤の皿を少し浮かしてとりつければかなり大きい面積が使えるというわけです。

用途に応じて、棒状でなくS型や丸型のロードセルも色々売られています。

 

さて、ありあわせの材料だけでテスト用のデジタル秤をチョコっと作ってみましたが、なんと楽なことか^^。秤量表示用のTM1637ディスプレイもセットしました。

f:id:a-tomi:20211008183117j:plain

まずは簡単なテストからやってみます。

①ゲージ側との接続

f:id:a-tomi:20211014143652j:plain

ロードセルのストレーンゲージから出ている赤,黒,緑,白の4つのワイヤーをHX711のE+,E-,A+,A-に接続。注意として、ストレーンゲージの配線は華奢(きゃしゃ)ですから、このときワイヤーの扱いに気を付けないといけませんね。私はロードセルから出ているワイヤーの根元を念のため事前にホットメルトで補強し、上の写真のようにワイヤークリップで台に固定してあまり動かないようにしました。

マイコン側との接続

Arduino Unoからの5VとGndをHX711の電源に供給します。信号線はどのデジタルピンでを選んでもよいですが、近くのピンを使うことにして、UnoのA1(=D15として使う)からHX711SCKのSCKへ、そしてDT(またはdOut)からUnoのA0(=D14として使う)とつなぎました。

f:id:a-tomi:20211008201413j:plain

そしてArduino Unoへ次の簡単なスケッチを書き込み、シリアルモニターでテストしました。

/********************************************************************************
*  Load Cell and HX711 testing for Arduino Uno  V.00          			*
*     modified sample on Oct. 2, 2021  by Akira Tominaga            		*
* Remarks: There are several libraries for HX711              			*
*          for the library used here, refer to;               			*
*https://www.instructables.com/How-to-Interface-HX711-Balance-Module-With-Load-Ce/
*********************************************************************************/
#include "HX711.h" 
#define OUT 14                	// Data from HX711 to A0=14
#define SCK 15              	// SCK to HX711 from A1=15
#define CalF 1885.F           	// individual LC caliblation factor   
HX711 LC(OUT, SCK);   		// construct class LC (gain = default 128)
    
void setup() { // ***** Arduino setup() *****
  Serial.begin(9600);
  Serial.println(F("* Load Cell *"));
  LC.set_scale(CalF);         	// set calibration factor(divider)
  LC.tare();      		// set zero for tare
}

void loop() { // ***** Arduino loop() *****
  Serial.println(LC.get_units(10), 3);
  LC.power_down();    		// set HX711 sleep mode
  delay(1000);
  LC.power_up();              	// wakeup HX711
}
// ***** end of test program *****

 

HX711用のArduinoライブラリーは各種ありますが、次のチュートリアルで紹介されているものが、今回のような秤を作る目的には一番楽そうでした。

Tutorial to Interface HX711 Balance Module With Load Cell : 9 Steps (with Pictures) - Instructables

これでテストし、おもり(大きな長ナットですが、校正済精密秤で19.385g)を途中で載せておろしたのが次のシリアル出力です。

* Load Cell *
-0.004 
0.008 
0.009 
0.004 
0.010 
0.008 
0.002 
0.013 
0.007 
-0.006 
0.007 
0.004 
0.008 
0.004 
16.990 
19.385 
19.383 
19.387 
19.385 
19.388 
19.377 
19.392 
19.393 
0.005 
0.008 
0.005 
0.008 
0.010 
0.013 
0.021 
0.011 
0.005 
0.014 
0.007 
0.003 
0.008 
0.003 

これをグラフにすれば次の通り。横軸は秒数で、今回はほぼ1秒間隔、(delayを外せば設定計測サイクルx10回ぶんの間隔)。

f:id:a-tomi:20211008202546j:plain

上は通常表示ですが、下は重さを対数軸にしたものです(よって負の点は表示されませんが)。これなら0.1g単位の表示はとても正確にできそうです。測定値は10回のサンプリング平均としていますが、これを増やせば0.01g単位での表示も可能だと思います。

なお、スケッチ中の

#define CalF 1885.F // individual LC caliblation factor

はロードセル毎のキャリブレーション値で、測定値をこれで割り算して重さ(g)に変換する除数(フロート型)です。重さの分かっている錘をあらかじめ計ることで値を決めます。

 

実に簡単とはいえ、とくに問題なく動いています。事前のネットでの簡単な調査ではノイズ問題の報告が色々みられました。一例は次のリンクです:

ひずみゲージADC HX711 のナゾシグナル | ココアシステムズ

ここで確認してみました。指摘時(2017年11月)はあった問題なのでしょうが、今では解決済みかと思われます。念のため在庫分の回転が速そうなセラーを選んたわけですが、シグナルをオシロで点検しておきました。

該当のノイズはどこにも出ていません。それをアナライザー(SCKをチャネル1、DataOUTをチャネル2)で記録したのが次です。

f:id:a-tomi:20211008204143j:plain

測定値の読取り部分(24ビット)を拡大すると次。

f:id:a-tomi:20211008204216j:plain

データシートによれば、以下が守られないといけません(守らないと誤読に至ります)。

①ADCが完了してから読取りを開始

Channel 1のクロック信号はArduino側から出しますが 、HX711がAD変換を終えてReadyになる(Data信号がLowになる)のを待ってから最初のHighを出して読取に入る。

②クロック信号のHighは60μS未満でなければならない。

観察結果は9.5μSですが時々最大15μSになります。しかし十分に収まっています。割り込み処理等が入る構造で1か所でもこれが守られないと誤読に至ります(60μS以上だとHX711がリセット信号とみなしリセット再開するためです)。

というわけでこれなら大丈夫。秤の簡単なテスト機を作ったのが次。本番機はもっとカッコよく、かなり小さく作れそうですね。

f:id:a-tomi:20211008204747j:plain

Arduino UnoからATmega328pだけを抜いて使っていますが、そのやり方説明が必要でしたら、次のリンク先の記事内に書いてありますのでご参照ください。

Zoom用かんたん操作ボタンをArduino-UNOで作る(その1) - 勝手な電子工作・・

本番用のスケッチは次ですが、あれっ?というほど簡単です。

/********************************************************************************
*  Digital Scale with Load Cell and HX711 for Arduino Uno  V.00    		*
*     Initial V.00      Oct.3, 2021  (c) Akira Tominaga   			*
* Remarks: There are several libraries for HX711        			*
*          for the library used here, refer to;       				*
*https://www.instructables.com/How-to-Interface-HX711-Balance-Module-With-Load-Ce/
*********************************************************************************/
#include "HX711.h"      		// see the above remarks
#include "TM1637Display.h"
#define hxOUT 14                        // Data from HX711 to A0=14
#define hxSCK 15                        // SCK to HX711 from A1=15
#define tmDIO 16      			// DIO of TM1637 to A2=16
#define tmCLK 17      			// CLK of TM1637 to A3=17
#define CalF 1885.F                     // caliblation factor (divider, float) 
HX711 LC(hxOUT, hxSCK);           	// class Load-Cell (gain: default 128)
TM1637Display TM(tmCLK, tmDIO);         // class TM for TM1637
float Wf = 0;                           // weight(gram) in float

void setup() {  // ***** Arduino setup() *****
  TM.setBrightness(0x0b);		// set up TM
  TM.showNumberDecEx(0, 0x00, true, 4, 0); // display 0000
  LC.set_scale(CalF);                   // set LC calibration factor
  LC.tare();        			// set zero with tare
}

void loop() {   // ***** Arduino loop() *****
  Wf = LC.get_units(10);		// get average measurement of 10 times
  int16_t Wx10i = round(Wf * 10);	// set unit 0.1 gram
  TM.showNumberDecEx(Wx10i, 0b00100000, false, 4, 0); // 4digits with a dot
  LC.power_down();      		// set HX711 into sleep mode
  delay(250);
  LC.power_up();                        // wake up HX711
} 
// ***** end of program *****

 

次の動画ではこれを使って、

①錘を置く位置に関係なく重さが正しく測れること

②計測がリニアである(足し算が成り立つ)こと

をかんたんに説明したつもり^^;  まずはクリックしてご覧いただくと幸い。

youtu.be

動画中で、分銅2つの合計の重さを計ったところがあります。

f:id:a-tomi:20211008205859j:plain

同じものを校正済の精密秤で計ると次でしたから正しいことが分かります。

f:id:a-tomi:20211008205929j:plain

その後200g~400gを測るなどテストしましたがとても正確!

計測後のCreep現象(荷重で歪みが残る現象)も、数分の計測では全く生じませんでした。やっぱり大したものです^^

 

以上、ごく簡単ながら、正確でコスパのよい精密秤作成のご紹介でした。

 

以下蛇足。ご興味ある方はご覧ください。

===============================

「ダブルベンディングビーム型ロードセル」がなぜ双眼鏡のような形で切り抜かれているかについて、少し補足させていただきたいと思います。

中に単なる直方体を切り抜けば、歪がどうなるかが直観でも分かりやすいのですが、そうやると切り抜いた角から材料にヒビが入っていく問題(応力集中)が生じます。なので、内部に角ができないように丸みをつけているもの。

この形だと力をかけた時の内部の歪(または内部応力)の計算が難解のため、50年ぐらい前から有限要素法(Fininte element method:有限の格子の集まりとしたモデルを使う構造計算)が使われています。格子の設定1例と計算結果は次です。

f:id:a-tomi:20211008211537j:plain

伸びと縮みのツボが一目瞭然です。赤と青の表面がストレーンゲージを貼るべき位置となります。今回使ったのは上下のツボ合計4か所に貼られています。

=================================

以上、ごく簡単な工作とテストでしたが、どなたかのお役に立てば幸いです。

 

2021.10.13追記:

①HX711のSleep modeは必要か?

スケッチのLoopの最後のところに入れましたが、これついて補記させていただきます。

  LC.power_down();    		// set HX711 sleep mode
  delay(1000);
  LC.power_up();              	// wakeup HX711

Sleep mode(Power_down, Power_up)は連続的な計測での省エネと温度変化の減少が目的ですが、単なる秤に使い電源オンも短い場合ならこれを入れなくてもよいかと思います。

長時間連続的に測る場合、ゲージ1枚につき

(I^2)*R = (V^2)/R = (2.5^2)/1000 = 6.25mW 

の発熱、4枚合計で25mW。しかしロードセルの熱伝導率が高いのでこれだと温度にほぼ影響ないと思われますが、HX711モジュール側の温度変化は測ってみないとわかりませんね。次の写真は、同じ板にロードセルを2つ並べ、その違いをテストしてみたものです。

計測の間に3秒のDelayをとり、左側のロードセルはその間ADCをSleepし、右側はSleepしないもの。環境は26℃で2時間後に温度を計測しました。

f:id:a-tomi:20211013112637j:plain

左右どちらのロードセルやゲージにも温度上昇はみられません。HX711モジュールは、Sleepさせていない右側だけが最大1.0℃上昇しました。ただし、測定値には何も影響はありませんでした(温度補償されているし、ゲージ側仕様でも通常の温度変化では影響がないものです)。ケースに収め長時間使うともう少し温度は上昇するとは思いますが。

②ノイズが入るものがある?

ネットで「ノイズで安定して計れない」という投稿や質問があるのですが、そういう場合は、念のために購入したゲージ貼付ロードセルを点検されることをお勧めします。私は10個求めたうちの2個で不良をみつけました。

点検と言っても、つなぐ前に赤黒緑白のどれか2つの間の抵抗をテスターで計ってみるだけです。テスターをつないだままでロードセルのワイヤー出口部分を軽く押すとノイズが出る(抵抗が変化する)ものがあります。

おそらく品質検査まではOKだったのでしょうが、とくに保護をせずに複数本をまとめて封筒で送って来るため、梱包時か送付中に引っ張られて不良を起こしたのではないかと推測します。取り出しのときもワイヤーをひっかけたりしないように注意しないといけませんね。せっかく良くできている部品なので。

 

この記事は次からご参照をいただいています:

dreamerdream.hateblo.jp

 

©2021 Akira Tomninaga, All rights reserved.

 

音声認識モジュールLD3320(Arduino Unoで)

f:id:a-tomi:20210927203533j:plain

音声認識スマホやアレクサ等々で普及しまくりましたが、マイコンでやろうとするとあまり簡単ではありません・・と思っていました。

今回試すのはvoice recognitionではなくspeech recognitionなのですが、日本語訳だとどちらも「音声認識」になるようです(・・?

「voice recognitionとspeech recognitionは明確に違う」と、例えば次のリンク先では英語で言っいるのですが、・・

https://www.totalvoicetech.com/difference-between-voice-recognition-and-speech-recognition/

ブラウザで翻訳すると「音声認識音声認識は違います・・・」と訳されるのでワケワカメ^^;

バイオメトリクス的な応用か、言っていることを解釈するのか、という違いでしょうが、今回はスピーチ認識。

LD3320モジュールは、話し手を選ばずに語句の識別と照合をしてくれるという、都合のよい音声認識モジュールです。

f:id:a-tomi:20210927204459j:plain

LD3320という専用 ICのブレークアウトモジュールで、内蔵マイク、外部マイク端子やスピーカー端子がついているもの。海外ネットでは千円強で買え、今見たら次のような感じです。

f:id:a-tomi:20210927204956j:plain

ICには"IC route"という社名が書かれているので検索してみると、ホームページがあってこの製品のデータシートやユーザーマニュアル類だけでなく、フォーラム風の情報も満載。全て中国語ですが今やブラウザーが上手に訳してくれるので十分使えますね。

IC routeのホームページは次で、この企業は主に音声認識ICを作っている会社のようです。

http://www.icroute.com/

LD3320モジュールはマイコンとSPIで接続しますが、信号は3.3Vでなければいけません。Unoの場合は、面倒でも5V~3.3Vの信号レベルコンバータを介します。8チャネルある次のコンバーターを使いました。CSをグランドに落とすだけにして節約しても信号5本は要るので、4チャネルでは無理そうですね。

f:id:a-tomi:20210927210042j:plain

接続の様子は次の写真のとおりですが、後ろにつけるプログラムをご覧いただくと明確でわかりやすいかと思います。

f:id:a-tomi:20210927210254j:plain

テストでは、Arduino-IDEのシリアルモニターで表示をしますが、一応LED4色で単純な表示もしてみます。

解釈する音声は50種までで、プログラムの立ち上がりで装置へ与えます。中国語ベースのためPinYin(といってもアクセントや抑揚はない)で入れます。

日本語や他の言語でも近い表現を入れれば認識できるというわけです。この例では「ありがとう」をいれたらバッチリでした。発音をWebでチェックしながら恐る恐るいれてみましたが^^; 

しかし、英語を入れるのはかなり難しいですね。英語はメロディ的でリズム感を要し、日本語のように子音母音だけでは決められない点、それに子音で中国語にはない発音も多いからでしょうかね。

Web検索しても英語の使用例にはたどり着かないのはそのためかも。

今回テストをしてみると、中国語はもちろん日本語も意外によく判定します。抑揚やアクセントの設定をとくにしないのに、よくもこんなことができるものです!

youtu.be

ほぼ何でも正しく解釈されるのに驚きますが、プログラムでいれてみた中国語の「黄色」の発音だけはうまく認識せず、結局スキップして別の表現を考え中。

今回作ったArduino Unoのスケッチは次です。語彙の登録も判定も全てLD3320内部でやってくれるので、マイコン側はとても小さな処理てすみます。

/********************************************************
   Speech-recognition test with LD3320 module
        V.01  Sept.25, 2021 by Akira Tominaga
   - Modified sample sketch of ld3320 library V2.0 at
     https://www.waveshare.net/study/article-11-1.html
    - Pin connections from LD3320 to Arduino Uno:
     Gnd-Gnd, 3V3-3.3V, all signal levels to be <=3.3V*
     MO-11, MI-12, SCK-13, CS/NSS-4, RST-9, IRQ-2, WR-Gnd
   - *Remarks: Logic Level Converter is required for Uno.
          As for LD3320, refer to http://www.icroute.com/
 ********************************************************/
#include "ld3320.h"   // installed from the above URL
VoiceRecognition Vc;  // voice recognition class as Vc
// LED sequence
#define LEDr 5        // LED red
#define LEDg 6        // LED green
#define LEDy 7        // LED yellow
#define LEDb 8        // LED blue
void setup() {        // ***** Arduino setup() *****
  pinMode(LEDr, OUTPUT); pinMode(LEDg, OUTPUT);
  pinMode(LEDy, OUTPUT); pinMode(LEDb, OUTPUT);
  digitalWrite(LEDr, LOW); digitalWrite(LEDg, LOW);
  digitalWrite(LEDy, LOW); digitalWrite(LEDb, LOW);
  Serial.begin(9600);
  Serial.println(F("Say keywords.."));
  Vc.init();                          // initialize Vc
  // key word, ID# (max.50, same ID# can be shared)
  Vc.addCommand("kai deng", 0);       // 開燈lights on
  Vc.addCommand("guan deng", 1);      // 關燈lights off
  Vc.addCommand("ohl ong", 0);        // all on
  Vc.addCommand("ohl off", 1);        // all off
  Vc.addCommand("hong se", 2);        // 紅色red
  Vc.addCommand("lu se", 3);          // 緑色green
  Vc.addCommand("huang se", 4);       // 黄色yellow
  Vc.addCommand("lan se", 5);         // 藍色blue
  Vc.addCommand("xie xie", 6);        // 謝謝
  Vc.addCommand("tank yu", 6);        // thank you
  Vc.addCommand("ahligatoh", 6);      // arigatoh
  Vc.start();                         // start Vc
}
void loop() {         // ***** Arduino loop() *****
  switch (Vc.read()) {
    case 0: Serial.println(F("On"));  // all LEDs on
      digitalWrite(LEDr, HIGH); digitalWrite(LEDg, HIGH);
      digitalWrite(LEDy, HIGH); digitalWrite(LEDb, HIGH);
      break;
    case 1: Serial.println(F("Off")); // all LEDs off
      digitalWrite(LEDr, LOW); digitalWrite(LEDg, LOW);
      digitalWrite(LEDy, LOW); digitalWrite(LEDb, LOW);
      break;
    case 2: Serial.println(F("red")); // LEDr on
      digitalWrite(LEDr, HIGH); break;
    case 3: Serial.println(F("green")); // LEDg on
      digitalWrite(LEDg, HIGH); break;
    case 4: Serial.println(F("yellow")); // LEDy on
      digitalWrite(LEDg, HIGH); break;
    case 5: Serial.println(F("blue")); // LEDb on
      digitalWrite(LEDb, HIGH); break;
    case 6: Serial.println(F("thanks"));
      for (uint8_t i = LEDr; i < LEDb + 1; i++) { // blink LEDs
        digitalWrite(i, HIGH);
        delay(500);
        digitalWrite(i, LOW);
        delay(500);
      }
      break;
    default: break;
  }
} // ***** end of program *****

日本語をPinYin表示するのは、中国語の心得がある人にとっては簡単なことかもしれませんが、中国語を勉強したことがない自分にとっては結構難しい。

そこで、Google翻訳の中国語欄にローマ字で日本語をいれて、中国語として発音させ、それらしく聞こえる綴りに変えてやってみたものです^^;

こうなるとせめてPinYin表だけでも勉強するのが良いかなと考え中。それと、メーカーのHPの中の説明には次のような記述があります。英語で使おうとするのはやめた方がいいかもしれません。

f:id:a-tomi:20210927212447j:plain

 

今回はマイコンで手軽にできる音声認識でした。作業で両手がふさがっているときの簡単な入力装置にすると便利そうです。

 

以上、何らかのお役にたてば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

簡単にできる指紋照合(Arduino-Unoで)2021.9.12にプログラム更新

f:id:a-tomi:20210903205017j:plain

殆どのスマホ指紋認証ができるようになりましたが、指紋は登録したくないという人が多いかもしれません。私もですがね^^; 指紋は個人が確実に特定できる優秀なバイオメトリクス。なのでネット機器に入れるのは躊躇してしまいます。

ところが、最近出回っている指紋センサーモジュールの性能とコスパが凄いのに気づいてしまいました。そうなると、どうしても試してみたくなりますね・・。もう長い長い自粛期間ですからね。

ネットにつながないDIY機器に使う目的なら、こんなに便利なものはない。安心して使ってみるということにしましょう^^;

指紋取込のしかけ

多く使われているのは、光学式、静電容量式、電界式など。スマホ内蔵センサーは静電容量式ですが、ここで使うのは光学式です。

f:id:a-tomi:20210903205320j:plain

説明が少し長くなりそうな予感がします。自分の記録用も兼ねた記事のためすみませんねm(_ _ )m

製作に直接関係しないところは青い文字で書いておきますので、お急ぎの方はそこは読み飛ばしてもかまいません。

光学式ではプリズムの45度斜面に指紋を当て二等面の片側からLED光を照射、反対側面から撮った像が加工されます。ガラス内の45度斜面なので光は全反射しますが、指紋の凸部(隆線)があたる部分だけが乱反射するのを利用して指紋を撮像しています。

f:id:a-tomi:20210903205501j:plain

上の写真は、前面すべてがプリズムの45度の斜面です。そこに指をあてるわけです。

使うセンサーモジュール

試すセンサーモジュールは海外ネットで購入。驚くほど低価格で売られていました。

f:id:a-tomi:20210903205603j:plain

“fingerprint”と&で“AS608”や”FPM-10a”などの検索語を使い、多く出回っていそうなものを選びました。注文後10日で届きました。書かれているカタログ仕様では、セキュリティレベルの設定(1~5)により、FAR(他人を通す間違いFalse Acceptance Ratio)は0.001%未満、FRR(本人を否定する間違いFalse Rejection Ratio)は1.0%未満にセットできます。

指紋登録は127種をセットできるので、1つの指を複数エントリー登録したりすればFRR(否定される間違い)の心配はないでしょうね。別の指を入れるのもいいですし。

指紋の自動照合システムは一昔前ならこの100~1000倍も高価だったと思います。今のしくみならAIが多少使われているかな?しかし調べた限り畳み込み層のマトリクスなどは使われていません。つまり長年の指紋照合技術が撮像素子やMCUの能力向上のおかげでこの性能とコストになった感じです。

このセンサーの場合、内蔵MCUはARM Cortex-MベースのSynoChip社AS608で、144MHz、Flash512KB、RAM128KB。データシートでみると通信インターフェイスはひととおりを装備しています。ただしモジュールの実装は次のようになっていて、UART接続が想定されています。製品によってコネクターのピンの並びが異なりますが、どれもUARTを想定している感じです。

f:id:a-tomi:20210903205752j:plain

さて、こういうDIY向きセンサーモジュールの使い方を調べる最近の格好のサイトは"EleCrow"だと思います。とくに中国で作られたものは満載です。

https://www.elecrow.com/wiki/index.php?title=Main_Page

Fingerprintセンサーのガイドとしては次のようなページがあります。

f:id:a-tomi:20210903205911j:plain

PCでの接続テスト

このサイトでWindows10 PC 用の”SFGdemo”プログラムが紹介され、無料でダウンロードできます。オープンソース・ハードの指紋センサー共通に使えるプログラムのようです。

なお、別途AdafruitのFingerprintライブラリーから同じプログラムがたどれますが、そこに示されているダウンロード対象は、なんとウィルスチェッカーで弾かれます。

EleCrowからのダウンロードは何も問題ありませんでした。

SFGdemoを使うには、PCとセンサーの間をシリアル通信で繋ぐ必要があります。センサーにシリアル~USBコンバータを接続して使ってももいいですが、Arduino-IDEでUSB接続したArduinoUnoをダミーで仲介役にすれば、より簡単ですしセンサーへの電源供給もできて便利ですね。

下に示すダミースケッチをIDEArduino Unoに書き込みIDEを終了します。次にセンサーのRxとTxの端子をそれぞれUnoの同名端子につなぎ、PCでSFGdemoを起動します。そのやり方やSFGdemo操作手順も、EleCrowではしっかり説明されています。

電源供給に注意センサーモジュールへの電源供給は仕様上は3.3Vでも5Vでも良いことになっていますが、使ったセンサーモジュールでは、5V供給でコミュニケーションエラーがしばしば生じていました。調べるとセンサーモジュールが返事をしない状態になります。PCBを観察するとQ6のレギュレータが省かれている感じなので、試しにArduino Uno側から3.3Vを供給してみたら、なんと問題は解消しました!(シリアル信号はそのままです、5V-tolerantのようです)。これはArduinoとの接続の場合も同じですから、製品によってはご注意ください。(2021.9.12追記:コネクターに3.3Vと直接書いてあるモデルなら、信号レベルコンバーターを介して信号線2本とも5Vから製品側を3.3Vに変換する方が安心ですね。それを指紋センサーの複数のモデルやってみましたらどれもそれでOKです。また、製品の若干の発熱もだいぶ減るので3.3Vがお勧めかも。)

PCとの接続テストに使ったダミープログラムは次です。

/***********************************************
 * Dummy sketch for Arduino as;
 *      PC-USB to sensor-Serial converter
 *      and a DC Power-Supply for sensors
 **********************************************/
void setup() {
}
void loop() {
}
// ***** end of sketch *****

 

指紋センサーモジュールが妥当に動くかをこれで調べるついでに、使う指をこのプログラムで登録しておくこともできますね。

f:id:a-tomi:20210903210242j:plain

取り込んだ指紋の画像は解析されバイナリーデータにして扱われます。つまり画像自体はメモリーに取り込まず、データ化されたものがセンサーモジュール内にテンプレートとして蓄積されます。

調べる対象の指紋もデータ化されてから使われます。そういうわけなので、蓄積データは少量ですみ、そこから画像自体を復元することはできませんしシステム的にはその必要もないわけです。

指紋の自動照合

指紋は隆線(凸部)と谷線(凹部)の高低差が約50μm、隆線間が約400μm。模様全体としての特徴もあります。そして幼児から何年たっても変わらない、さらにDNAが同じ双子でも違うという驚くほど精巧なバイオメトリクスですね。

指紋照合には、2世紀以上にわたる知恵により、各指紋の「特徴点」(minutiaマイヌーシア)が現代でも使われています。

f:id:a-tomi:20210903210549j:plain

自動照合の歴史上は指紋の中央点、デルタ部分、ドットなど他の特徴点、あるいは特徴点間の隆線数など、撮像品質や処理性能を補う工夫がなされたものもあります。しかし現代(参照:ISO/IEC19794-2:Cor.1:2012)では、例えば上図に◎で示したような限られた特徴点だけでも照合に十分です。

国際標準では特徴点のxy座標と隆線の方向(右水平からの時計回り角度t)の3データ(x,y,t)が使われます。

xy座標原点は左端中央ですが、(x,y,t)は原点の移動も照合角度の調整も演算だけで簡単にできるわけなので、どこに設定してもよいわけです。特徴点は1指紋がもつ通常50~150点の中の数十だけ(統計学的には12点以上でよい)を使えば十分。ISOの指紋テンプレートではx,yを各2バイト(14ビット)、方向を1バイト、それに品質1バイトとし、1特徴点を6バイトに収めます。ただし中身がどのシステムも共通かと言えばそうではなく、原点位置やアルゴリズムなどにも依存するものです。

照合する際の1指紋は隆線が読みやすいように加工され、このセンサーの場合は検査対象の数十の特徴点が128バイトに収められています。テンプレートファイルとしても1つの指(ID)登録に512バイトです。画像を直接蓄えるよりもずっと効率が良いわけ。

特徴点による指紋照合の原理の発案は、実に19世紀!英国のゴルトン(Sir Francis Galton)が現代の認証技術に使われているMinutia(マイヌーシア=特徴点)での照合法を見出しました。著書Finger Prints(1892年)に詳しく書かれています。分厚いですが次のリンクから無料でダウンロード可能です。

https://galton.org/books/finger-prints/galton-1892-fingerprints-1up.pdf

そして、照合の自動化は1970年にNBS(米国標準局)のJ. H. Wegsteinらが初めて開発しました。この原理が現在の照合方法のベースとして長年使われてきています。このセンサーモジュールでも同じことです。

https://nvlpubs.nist.gov/nistpubs/Legacy/TN/nbstechnicalnote538.pdf

f:id:a-tomi:20210903210938j:plain

余談が長引きましたがここからようやく本題です^^;

センサーに当てる指の向き

指紋センサーにあてる指先はおそらく上向きだろうと想像しますが、モジュール単体だと角度が邪魔をして難しいです。テストのために適当な台をつけてみました。

f:id:a-tomi:20210903211110j:plain

PCにつないでセンサーを確認

上で少し説明しましたが、Arduinoをツールとして使い、センサーをPCと直接シリアル接続する際の様子は次の写真です。使う端子はV+、Gnd、Tx、Rxだけなので他の余計なワイヤーは後でソケットからピンごと抜き取るとか、面倒ならカットしてもいいですね。センサーへの電源供給は前に書いたとおり最終的には3.3Vに変更しました。

f:id:a-tomi:20210903211211j:plain

PCでのテストは全てうまく動きました。SFGdemoでは指紋の映像が見えるので、適切な指の当て方などがわかりました。そして、台に載せてわざわざ指を上向きに当てなくとも、センサーをつかむ形で指を下向きにして登録しても、照合時も同じ向きにすれば何ら問題ないことがわかりました^^

PCでのこのテストではセンサーのTx,RxをそれぞれArduinoピンのTx,Rxにつなぎますが、以後のArduino自体でのテストでのソフトウェアシリアルで使うTx,Rxピンでは逆につなぎますから十分ご注意ください。

 

Arduinoでのテスト開始

ここからはいよいよArduinoでのテストをします。センサーからのTxをArduino―UnoのPin2(ソフトウェアシリアルのRx)へ、RxをPin3(ソフトウェアシリアルのTx)へ接続し直します。

まずは指紋の登録(Enroll)です。

Adafruit_Fingerprintライブラリーのスケッチ例"Enroll"がそのまま使えます。ただし、グローバル変数を922バイト(Unoの45%)も使う状況では、後での好きな加工には支障が出そうです。

「最大32256バイトのフラッシュメモリのうち、スケッチが7002バイト(21%)を使っています。最大2048バイトのRAMのうち、グローバル変数が922バイト(45%)を使っていて、ローカル変数で1126バイト使うことができます。」

よって書き換えて次のように減量しておきました。これだとグローバル変数の18%しか使いません。

(この型の指紋センサーの他のモデルでも動くよう、スケッチを2021.09.12に更新しました。それをつけます。)

/*******************************************************************
   Fingerprint enrollment test V.01  as of Sept.12,2021
      Initial version: Aug.23, 2021  modified by Akira Tominaga
      V.01: Retry-interval shortened for all FP models * 9/12/'21
      using Adafruit_Fingereprint library and example,
      with BSD license, described in the bottom of this sketch.
*******************************************************************/
#include "Adafruit_Fingerprint.h"
#define sRx 2
#define sTx 3
SoftwareSerial sSer(sRx, sTx);
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&sSer);
uint8_t id;           // ID# for fingerprint

void setup() { // ***** Arduino setup() *****
  Serial.begin(9600);
  while (!Serial) {}
  delay(100);
  Serial.println(F("\n*Enroll*"));
  finger.begin(57600);
  if (finger.verifyPassword() == false) {
    Serial.println(F("No sensor :("));
    while (true) {}
  }
  Serial.print(F("Sensor"));
  finger.getParameters();
  Serial.print(F(" Stat=0x")); Serial.print(finger.status_reg, HEX);
  Serial.print(F(" ID=0x")); Serial.print(finger.system_id, HEX);
  Serial.print(F(" Capa=")); Serial.print(finger.capacity);
  Serial.print(F(" Sec.lvl=")); Serial.print(finger.security_level);
  Serial.print(F(" Dev.adr=")); Serial.print(finger.device_addr, HEX);
  Serial.print(F(" Pkt=")); Serial.print(finger.packet_len);
  Serial.print(F(" Baud=")); Serial.println(finger.baud_rate);
}

void loop() { // ***** Arduino loop() *****
  Serial.println(F("\nGive ID#(1-127) to set"));
  id = readnumber();
  if ((id == 0) | (id > 127)) { // invalid #, retry!
    return;
  }
  Serial.print(F("#"));
  Serial.println(id);
  while (!getFPtoEnroll() );
}
/*********************************************
 *    User defined functions
 *********************************************/
// ***** read number from Serial input *** readnumber() *****
uint8_t readnumber(void) {
  uint8_t num = 0;
  while (num == 0) {
    while (! Serial.available());
    num = Serial.parseInt();
  }
  return num;
}
// ***** get fingerprint enroll ***  getFPtoEnroll() *****
uint8_t getFPtoEnroll() {
  int p = -1;
  Serial.println(F("Place finger")); 
  while (p != FINGERPRINT_OK) {
    p = finger.getImage();
    switch (p) {
      case FINGERPRINT_OK:
        Serial.println(F("Img taken"));
        break;
      case FINGERPRINT_NOFINGER:
        break;
      case FINGERPRINT_PACKETRECIEVEERR:
        Serial.println(F("Com.err"));
        break;
      case FINGERPRINT_IMAGEFAIL:
        Serial.println(F("Img err"));
        break;
      default:
        Serial.println(F("Unknown err"));
        break;
    }
  }
  // OK success!
  p = finger.image2Tz(1);
  switch (p) {
    case FINGERPRINT_OK:
      Serial.println(F("Img Okay")); 
      break;
    case FINGERPRINT_IMAGEMESS:
      Serial.println(F("Img messy"));
      return p;
    case FINGERPRINT_PACKETRECIEVEERR:
      Serial.println(F("Com.err"));
      return p;
    case FINGERPRINT_FEATUREFAIL:
      Serial.println(F("No feature"));
      return p;
    case FINGERPRINT_INVALIDIMAGE:
      Serial.println(F("Img err"));
      return p;
    default:
      Serial.println(F("Unknown err"));
      return p;
  }
  Serial.println(F("Remove finger"));
  delay(2000);
  p = 0;
  while (p != FINGERPRINT_NOFINGER) {
    p = finger.getImage();
  }
  Serial.print("For ID#"); 
  Serial.print(id);
  p = -1;
  Serial.println(F(", same finger again"));
  while (p != FINGERPRINT_OK) {
    p = finger.getImage();
    switch (p) {
      case FINGERPRINT_OK:
        Serial.println(F("Img taken"));
        break;
      case FINGERPRINT_NOFINGER:
        break;
      case FINGERPRINT_PACKETRECIEVEERR:
        Serial.println(F("Com.err"));
        break;
      case FINGERPRINT_IMAGEFAIL:
        Serial.println(F("Img err"));
        break;
      default:
        Serial.println(F("Unknown err"));
        break;
    }
  }
  // OK success!
  p = finger.image2Tz(2);
  switch (p) {
    case FINGERPRINT_OK:
      Serial.println(F("Img converted"));
      break;
    case FINGERPRINT_IMAGEMESS:
      Serial.println(F("Img messy"));
      return p;
    case FINGERPRINT_PACKETRECIEVEERR:
      Serial.println(F("Communication .err"));
      return p;
    case FINGERPRINT_FEATUREFAIL:
      Serial.println(F("No feature"));
      return p;
    case FINGERPRINT_INVALIDIMAGE:
      Serial.println(F("Img err"));
      return p;
    default:
      Serial.println(F("Unknown err"));
      return p;
  }
  // OK converted!
  Serial.print(F("Checking FP#"));  Serial.println(id);
  p = finger.createModel();
  if (p == FINGERPRINT_OK) {
    Serial.println(F("Matched!"));
  } else if (p == FINGERPRINT_PACKETRECIEVEERR) {
    Serial.println(F("Com.err"));
    return p;
  } else if (p == FINGERPRINT_ENROLLMISMATCH) {
    Serial.println(F("No match"));
    return p;
  } else {
    Serial.println(F("Unknown err"));
    return p;
  }
  Serial.print(F("*** FP#")); Serial.print(id);
  p = finger.storeModel(id);
  if (p == FINGERPRINT_OK) {
    Serial.println(F(" enrolled! ***"));
  } else if (p == FINGERPRINT_PACKETRECIEVEERR) {
    Serial.println(F("Com.err"));
    return p;
  } else if (p == FINGERPRINT_BADLOCATION) {
    Serial.println(F("Bad loc."));
    return p;
  } else if (p == FINGERPRINT_FLASHERR) {
    Serial.println(F("Flash err"));
    return p;
  } else {
    Serial.println(F("Unknown err"));
    return p;
  }
  return true;
}
/*******************************************************************
  This is an example sketch for our optical Fingerprint sensor
  Designed specifically to work with the Adafruit BMP085 Breakout
  ----> http://www.adafruit.com/products/751
  These displays use TTL Serial to communicate, 2 pins are required to
  interface
  Adafruit invests time & resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!
  Written by Limor Fried/Ladyada for Adafruit Industries.
  BSD license, all text above must be included in any redistribution.
*******************************************************************/
// ***** end of program *****

 

指紋の登録

そして指を登録(Enroll)しますが、テストでは右手は操作に使うので、左手の人差し指、中指、薬指を登録しました。念のためにそれぞれIDを3つ登録して、人差し指(1,11,21)、中指(2,12,22)、薬指(3,13,23)としておきました。こうすれば多少乱暴に指をあててもRejectされて困るようなことはないでしょうからね。

Arduino IDEのシリアル出力は次のようなものになります。


*Enroll*
Sensor Stat=0x0 ID=0x0 Capa=150 Sec.lvl=3 Dev.adr=FFFFFFFF Pkt=128 Baud=57600

Give ID#(1-127) to set
#1
Place finger
Img taken
Img Okay
Remove finger
For ID#1, same finger again
Img taken
Img converted
Checking FP#1
Matched!
*** FP#1 enrolled! ***

Give ID#(1-127) to set
#2
Place finger
Img taken
Img Okay
Remove finger
For ID#2, same finger again
Img taken
Img converted
Checking FP#2
Matched!
*** FP#2 enrolled! ***

Give ID#(1-127) to set

 

指紋の照合

次に、指紋の照合(Identify)をします。

Adafruit_Fingerprintライブラリーのスケッチ例Fingerprintがそのまま使えますが、グローバル変数を同様にたっぷり使うため、そうはいきません。こちらは今後確実に応用に使うわけなので。次のように大きく減量して必要最小限に書き換えました。十分小さいですね!これならUnoでも余裕たっぷりです。エラー検出時の処理は、本番ではどっちみち再処理を実行するわけなので、余計なメッセージ出力を削減。

(この型の指紋センサーの他のモデルでも動くよう、スケッチを2021.09.12に更新しました。それをつけます。)

/*******************************************************************
   Fingerprint identification test V.01
      Initial version: Aug.23, 2021  modified by Akira Tominaga
      V.01: Retry-interval shortened for all FP models   * 9/12/'21
      using Adafruit_Fingereprint library and examples,
      with BSD license, described in the bottom of this sketch.
*******************************************************************/
#include "Adafruit_Fingerprint.h"
#define sRx 2
#define sTx 3
SoftwareSerial sSer(sRx, sTx);
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&sSer);

void setup() { // ***** Arduino setup() *****
  Serial.begin(9600);
  Serial.println(F("\n*FP identification*"));
  finger.begin(57600);
  if (finger.verifyPassword() == false) {
    Serial.println(F("No sensor :("));
    while (true) {}
  }
  finger.getTemplateCount();
  if (finger.templateCount == 0) {
    Serial.print(F("No template"));
    while (true) {};
  }
}
void loop() { // ***** Arduino loop() *****
  Serial.print(F("\nPlace finger."));
  while (finger.getImage() != FINGERPRINT_OK) {
  }
  while (finger.image2Tz() != FINGERPRINT_OK) {
  }
  uint8_t p = finger.fingerSearch();
  if (p == FINGERPRINT_OK) {
    uint8_t fID = finger.fingerID;
    uint8_t fCf = finger.confidence;
    Serial.print(F("\n*** Match! with #")) ;
    Serial.print(fID);
    Serial.print(F(" confidence="));
    Serial.println(fCf);
    delay(5000);
  } else if (p == FINGERPRINT_NOTFOUND) {
    Serial.println(F("\n*** No match!"));
    delay(1500);          // wait after unmatch
  } else {
  }                       // in other cases, do nothing
  delay(500);             // loop in a slow pace
}
/*******************************************************************
  This is an example sketch for our optical Fingerprint sensor
  Designed specifically to work with the Adafruit BMP085 Breakout
  ----> http://www.adafruit.com/products/751
  These displays use TTL Serial to communicate, 2 pins are required to
  interface
  Adafruit invests time & resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!
  Written by Limor Fried/Ladyada for Adafruit Industries.
  BSD license, all text above must be included in any redistribution.
*******************************************************************/
// ***** end of program *****

 

これで照合をするとArduino IDEのシリアルに結果が正しく出ます。指の置き方によってリジェクト(FRR)が出ることがありますが、指の置き方をやり直せばよいだけです。

間違い容認(FAR)のほうは色々やっても全く生じません。たいしたものです!!

Arduino IDEのシリアル出力は次のようなものです。


*FP identification*

Place finger.
*** No match!

Place finger.
*** Match! with #1 confidence=169

Place finger.

Confidenceは合致のスコア集計数値です。50ぐらい以上が合致とされるようですが、セキュリティレベルの設定によってその閾値が決まるようです。セキュリティレベルの設定は今後やってみることにします。

 

プロトコルの確認

センサーとはどのようなやり取りをしているでしょうか。オシロで観察します。

最初のパスワード確認プロトコルは次のもの。

f:id:a-tomi:20210903212641j:plain

意外に単純そうですね。それなら、というわけでロジックアナライザーでやり取りを確認してみました。これは照合ですがプロトコルはわりと単純で意外にスピーディです。

f:id:a-tomi:20210903212731j:plain

f:id:a-tomi:20210903212743j:plain

全体をデコードした例が次です。

	Time [s]	Decoded 
	Tx	
	0.0000000	0xEF
	0.00018625	0x01
	0.0003795	0xFF
	0.00056525	0xFF
	0.00075125	0xFF
	0.00093725	0xFF
	0.0011230	0x01
	0.00131675	0x00
	0.00150425	0x07
	0.00169225	0x13
	0.0018805	0x00
	0.00206925	0x00
	0.0022580	0x00
	0.0024530	0x00
	0.00264150	0x00
	0.00282950	0x1B
	Rx	
	0.00395725	0xEF
	0.00414875	0x01
	0.00434025	0xFF
	0.00453175	0xFF
	0.00472325	0xFF
	0.0049150	0xFF
	0.0051065	0x07
	0.0052980	0x00
	0.0054895	0x03
	0.0056810	0x00
	0.0058725	0x00
	0.0060640	0x0A
	Tx	
	0.00634925	0xEF
	0.0065415	0x01
	0.00672875	0xFF
	0.0069145	0xFF
	0.0070945	0xFF
	0.00728025	0xFF
	0.00747225	0x01
	0.0076600	0x00
	0.00784725	0x03
	0.00803575	0x1D
	0.0082235	0x00
	0.0084115	0x21
	Rx	
	0.00975175	0xEF
	0.00994325	0x01
	0.0101350	0xFF
	0.0103265	0xFF
	0.0105180	0xFF
	0.0107095	0xFF
	0.01090125	0x07
	0.01109275	0x00
	0.01128425	0x05
	0.01147575	0x00
	0.0116675	0x00
	0.0118590	0x09
	0.01205075	0x00
	0.01224225	0x15
	Tx	
	0.01254425	0xEF
	0.0127365	0x01
	0.01292375	0xFF
	0.0131095	0xFF
	0.0132955	0xFF
	0.0134815	0xFF
	0.01368125	0x01
	0.01386875	0x00
	0.01405625	0x03
	0.0142445	0x01
	0.01443275	0x00
	0.01463475	0x05
	Rx	
	0.095171	0xEF
	0.0953625	0x01
	0.095554	0xFF
	0.0957455	0xFF
	0.095937	0xFF
	0.0961285	0xFF
	0.096320	0x07
	0.0965115	0x00
	0.096703	0x03
	0.0968945	0x02
	0.097086	0x00
	0.0972775	0x0C

 

データシートに基づいてコマンドとレスポンスをよく整理すれば、Arduino以外のマイコンでも使えるようになりそうですね。今後のお楽しみとして。。

単体で使える形に

ここまでで、このセンサーが優れものであることと、うまく扱えることがよくわかりました。

Arduino単体で動かす例として、とりあえずはLCD出力をしてみました。それがこの記事冒頭の写真です。テストの様子の動画は次です。

youtu.be

この例のスケッチは次です。

(この型の指紋センサーの他のモデルでも動くよう、スケッチを2021.09.12に更新しました。それをつけます。)

/*******************************************************************
   Fingerprint identification & show the results to LCD   V.01
      Initial version: Aug.23, 2021  modified by Akira Tominaga
      V.01: Retry-interval shortened for all FP models * 9/12/'21
      using Adafruit_Fingereprint library and examples,
      with BSD license, described in the bottom of this sketch.
*******************************************************************/
#include "Adafruit_Fingerprint.h"
#include "Wire.h"
#include "LiquidCrystal_I2C.h"
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set address and pins
#define lcdC 20
#define lcdL 4
#define sRx 2
#define sTx 3
SoftwareSerial sSer(sRx, sTx);
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&sSer);

void setup() { // ***** Arduino setup() *****
  Serial.begin(9600);           // init. h/w serial for debug
  lcd.begin(20, 4);             // init. 20 x 4 LCD
  lcd.backlight();              // turn on lcd-backlight
  lcd.setCursor(0, 0);
  lcd.print(F("* Fingerprint test *"));
  lcd.setCursor(1, 3);
  lcd.print(F("(a-tomi.hatenablog)"));
  finger.begin(57600);          // init sSer and finger
  if (finger.verifyPassword() == false) {
    lcd.setCursor(3, 1);
    lcd.print(F("Error: No sensor."));
    while (true) {}
  }
  finger.getTemplateCount();
  if (finger.templateCount == 0) {
    lcd.setCursor(1, 1);
    lcd.print(F("Error: No template."));
    while (true) {};
  }
}
void loop() { // ***** Arduino loop() *****
  lcd.setCursor(2, 1);
  lcd.print(F("Place finger...   "));
  while (finger.getImage() != FINGERPRINT_OK) {
    lcd.setCursor(2, 2);
    lcd.print("  ...             ");
  }
  while (finger.image2Tz() != FINGERPRINT_OK) {
    lcd.setCursor(2, 2);
    lcd.print("  ,,,              ");
  }
  uint8_t p = finger.fingerSearch();
  if (p == FINGERPRINT_OK) {
    uint8_t fID = finger.fingerID;
    uint8_t fCf = finger.confidence;
    lcd.setCursor(2, 1);
    lcd.print(F("Match! with ID="));
    lcd.print(fID);
    lcd.setCursor(2, 2);
    lcd.print(F("confidence = "));
    lcd.print(fCf);
    lcd.print("  ");
    delay(5000);
  } else if (p == FINGERPRINT_NOTFOUND) {
    lcd.setCursor(2, 1);
    lcd.print(F("No match!         "));
    lcd.setCursor(2, 2);
    lcd.print(F("                  "));
    delay(2500);          // wait after unmatch
  } else {
  }                       // in other cases, do nothing
  delay(500);             // loop in a slow pace
}
/*******************************************************************
  This is an example sketch for our optical Fingerprint sensor
  Designed specifically to work with the Adafruit BMP085 Breakout
  ----> http://www.adafruit.com/products/751
  These displays use TTL Serial to communicate, 2 pins are required to
  interface
  Adafruit invests time & resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!
  Written by Limor Fried/Ladyada for Adafruit Industries.
  BSD license, all text above must be included in any redistribution.
*******************************************************************/
// ***** end of program *****

 

おまけですが、単にLEDでどの指かを示す装置。

f:id:a-tomi:20210903213537j:plain



おわりに

これなら指紋認証ロックなどはとても簡単にできそうですね。そのうち応用したいと思います。

以上、自分用の記録も兼ねて少し冗長でしたが、何らかのお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

 

LEDテープで大ディスプレイを! (LEDテープその3)

f:id:a-tomi:20210818170901j:plain

お馴染みピクトグラムArduinoが次々に描きます。このディスプレイは安価なLED配列フィルムですが、Arduinoから1ワイヤで高速に描画できます。

これまでに書いたLEDテープ記事の続編ですが、前の2つの記事のリンクは次です。

カラーLEDテープ照明を自由にコントロール - 勝手な電子工作・・

LEDテープを使ったディスプレイの実験(Arduino-Unoで) - 勝手な電子工作・・

 

次のような大型、といっても8x32で256個のLEDが配列された柔らかいシートが売られているのを見つけました。AliExpressで1枚約800円でしたが、注文したら珍しく翌週にサッと到着。LEDテープというよりLEDシートというべきかもしれません。

f:id:a-tomi:20210818172350j:plain

しめしめ、これを3段つなげば32x24ドットができるぞ!

毎度ながら簡単・迅速に作りたいですし、また、後で部品に戻せるようにしたいもの。ここでは両面テープでプラバンに貼り付け、裏から配線することにします。

f:id:a-tomi:20210818172537j:plain

真ん中は電源供給ラインを並列につなぎます。信号線は最初の入り口は左上に(信号線とグランドのみ)が入り、右上から出た最後を真ん中左へ(信号線のみ1本でOK、グランドは電源側で接続されているわけなので)と配線。同じく中右から出た最後を左したへと配線して終わり。じつに単純ですね^^ 動かないようにセロテープで止めて出来上がり。電源は5V2AのACアダプターで十分でした。

32x24とはいえ、24x32と見る方が分かりやすい。とはいってもLEDの接続順序は表から見ると次のように少し複雑になります。これをXY座標として扱って、プログラム論理でこれにあうようにすればよいだけです。

f:id:a-tomi:20210818173422j:plain

柔らかいシートとはいっても、両面テープは時間が経つと、次の写真のようにジワリと剥がれて浮くところが少しでます。強力両面テープを使えばこうはなりませんが、後で分解しいやすくするためには、この程度ですかね。

f:id:a-tomi:20210818173939j:plain

見易さを兼ねて、たまたま手元にある薄黒い半透明アクリル板をこの上に重ねて使ってみます。なにせLEDは相当に眩しいですから。また、こうすることでディスプレイの平面も浮くことなく保てています。

FastLEDライブラリーは容量を食わずに便利ですが、この場合はLED数768個x3バイト(3色の値)ぶんの配列を内部でこしらえるため、ArduinoはMega2560を使うことにします。

画像データはPCのシリアルから送る方法でもよいと思いますが、持ち運び用に、ここではマイクロSDカードから入力することにしました。

1つのファイルが画面1頁です。中身は3色のデータ#RRGGBBを768個収容したファイルで、ファイル名を順次変化させることで、自動的にページを順番に読ませることにしました。

そのArduinoスケッチは後ろにつけておきます。

簡単にファイルを作るために、画像を24x32ピクセルBMPにして、それをPCで読み込ませます。簡単なSmallBasicでテスト用にちょこっと作ったWindows用のプログラムですが、ご参考のため記事の最後に添付しておきます。

そして表示の一例が次です。

f:id:a-tomi:20210818174951g:plain

実際はとても綺麗なのですが、LEDの輝度が高過ぎてうまく撮れませんm(_ _ )m

活字も、「ドットフォント」を絵と同じように扱えば大丈夫。ここで使っているドットフォントはフリーのKHドットフォントのなかにある、小伝馬町16というフォントです。True Typeフォントがしっかり提供されていますので、絵として使うのにも適しています。色々便利に使わせていただいています。OLEDなどの表示も画像としてとり扱うと手間がかからないのでなかなかいい手段ではないかなと勝手に思います。もちろんコードで直接扱うのがベストではありますが。

 

次がArduino-Megaのスケッチです。

/********************************************************
   LED panel display for Arduino-Mega
      Read & show RGB data from "RGBtnnn.txt" files.
      Version 00  Aug.8, 2021 by Akira Tominaga
*********************************************************/
#include "FastLED.h"
#include "SPI.h"
#include "SD.h"
#define dPin 8                // data pin to LED tape
#define CS 53                 // SD MISO=50,MOSI=51,SCK=52
uint16_t X = 0, Y = 0, Pnl = 0; // column, row, and panel
#define maxX 24               // maximum X in total
#define maxY 32               // maximum Y
#define nPnls 3               // nnumber of panels (films) 
#define pnlX (maxX/nPnls)     // X length of each panel
#define NinPnl (pnlX*maxY)    // number of LEDs in a panel   
#define nLEDs (NinPnl*nPnls)  // number of total LEDs
CRGB myTp[nLEDs];             // construct myTp array
File mySD;                    // construct mySD
String fNbase = "RGBt";       // for file name RGBtnnn.txt
String fNext = ".txt";        // file name extention
uint16_t fNum = 0;            // file number = page (max999)
char fN[4];                   // file number in char
String fName = "";            // file name
#define fRlen 9               // file rec length 7char+CRLF
char SDin[fRlen + 1];         // SD-in area with CRLF + Null
uint8_t vRGB[3];              // temporally RGB work data

void setup() { // ***** Arsuino setup() *****
  Serial.begin(9600);
  delay(2000);                // enough time for LED pwr
  if (!SD.begin(CS)) {
    Serial.println(F("SD err."));
    while (true);
  }
  FastLED.addLeds<WS2812, dPin, GRB>(myTp, nLEDs);
  clrLED();
}
void loop() { // ***** Arduino loop() *****
  setFname();                 //next file name to open
  Serial.println(fName);
  clrLED();
  XY2LED();       // read RGB@(X,Y) and set to LED positions
  FastLED.show();             // show the page
  delay(1000);                // time to display a page
  mySD.close();               // close current page
  fNum++;                     // set next file(=page) number
}
/************************************************
    User defined functions
 ************************************************/
// ***** clear LED *** clrLED() *****
void clrLED(void) {
#define Black (0,0,0)
  for (int i = 0; i < nLEDs; i++) {
    myTp[i].setRGB Black;
  }
  FastLED.show();
}
// ***** set file name of next page *** setFname() *****
void setFname(void) {
  sprintf(fN, "%03d", fNum);
  fName = fNbase + String(fN) + fNext;
  mySD = SD.open(fName);      // open a page
  if (!mySD) {                // if file not found
    Serial.print(F("No "));
    Serial.println(fName);
    if (fNum > 0) {           // if no more page
      delay(1000);            // wait for a second
      fNum = 0;               // and return to page 0
      fName = fNbase + "000" + fNext; // restart from p.0
      mySD = SD.open(fName);  // and open page 0
    } else {                  // if page 0 file not found
      while (true) {}         // then stop
    }
  }
}
// ***** get RGB[](X,Y) val and set to LED pos. *** XY2LED() *****
void XY2LED(void) {
  uint16_t sLED;              // sequence # of LED from the top 
  for (Y = 0; Y < maxY; Y++) {
    for (X = 0; X < maxX; X++) {
      Pnl = X / pnlX;         // get panel number
      uint16_t nC = X % pnlX; // get X (column #) in that panel
      readRGB();              // read record for RGB values
      if (Y % 2 == 1) {       // for Ys 1,3,5,,,
        sLED=nC + Pnl * NinPnl + Y*pnlX;
      } else {                // for Ys 0, 2,4,,, reverse X sequence
        sLED=(pnlX - 1 - nC) + Pnl * NinPnl +  Y*pnlX;
      }
      myTp[sLED].setRGB(vRGB[0], vRGB[1], vRGB[2]);
    }
  }
}
// ***** read SD for RGB data *** readRGB() ***
void readRGB(void) {
  while (!mySD.available()) {}
  mySD.read(SDin, fRlen);     // read a record
  for (int i = 0; i < 3; i++) {  // for RGB data
    byte Vh = SDin[i * 2 + 1] & 0x0F; // if numeric, get value
    if (SDin[2] > 0x40) {     // if alfabet,
      Vh = SDin[i * 2 + 1] - 55; // set alphabetic value
    }
    byte Vl = SDin[i * 2 + 2] & 0x0F; // if numeric, get value
    if (SDin[2] > 0x40) {     // if alfabet,
      Vl = SDin[i * 2 + 2] - 55; // set alphabetic value
    }
    vRGB[i] = (Vh * 16 + Vl) / (3); // divide R|G|B w/ 3 
  }
}
// ***** end of program *****

 

読み込むファイルには次の形式の固定長レコードが、LEDの個数分並んでいます。

#RRGGBB\n

RR、GG、BBは赤、緑、青の強さで、0からFFで表します。しかし、そのままではLEDが明るすぎるため、Arduinoで読んだら各値を1/3に落としています。節電にも重要ですね。

ファイル1個が画面の1頁となり、Arduinoはファイル番号を増やしながら次々読み、次のファイルがなくなると最初に戻ります。

そのファイルをWindowsPCでBMP画像から自動生成する簡単なプログラムは次です。Small Basicでちょこっと作ったテスト用ですが。

myTitle="RGB text builder   Aug.8,2021 (c)Akira Tominaga"
TextWindow.Title=myTitle
TextWindow.BackgroundColor="Black"

' Get data path
myPath=File.GetSettingsFilePath()
myPath=Text.GetSubText(myPath,1,8)
myPath=myPath+"DatW\"

' Prepare picture
TextWindow.WriteLine("")
KeyIn:
TextWindow.Write("     Please type-in RGBt#. -> ")
keyinN=TextWindow.Read()
myPict=myPath+"RGBt"+keyinN+".bmp"
PsizeX=ImageList.GetWidthOfImage(myPict)
If PsizeX>5 Then
  Goto ImageOK
Else
  TextWindow.Write("*** No such bmp as ")
  TextWindow.WriteLine(myPict)
  Goto Keyin
EndIf
ImageOK:
PsizeY=ImageList.GetHeightOfImage(myPict)
TextWindow.Write("Xsize=")
TextWindow.WriteLine(PsizeX)
TextWindow.Write("Ysize=")
TextWindow.WriteLine(PsizeY)

'Setup output file
myFile=myPath+"RGBt"+KeyinN+".txt"
'lnum=1 'PsizeX,PsizeY (3chars left-zero)
Xs=Text.GetSubText(1000+PsizeX,2,3)
Ys=Text.GetSubText(1000+PsizeY,2,3)
header=Xs+","+Ys
'File.WriteLine(myFile,lnum,header)
lnum=0

'Graphics window
GraphicsWindow.BackgroundColor = "Black"
GraphicsWindow.Width=PsizeX*45
GraphicsWindow.Height=PsizeY*22
GraphicsWindow.Title=myTitle
inPic=ImageList.LoadImage(myPict)
GraphicsWindow.DrawImage(inPic,0,0)
For j= 0 to PsizeY-1
  For i=0 To PsizeX-1
    color=GraphicsWindow.GetPixel(i,j)
     TextWindow.WriteLine(Color)
     lnum=lnum+1
     File.WriteLine(myFile,lnum,Color)
     
    'Draw big image
    XXb=PsizeX+2
    YYb=PsizeY+2
    XX=i*20+XXb
    YY=j*20+YYb
    GraphicsWindow.BrushColor=Color
    GraphicsWindow.FillRectangle(XX,YY,20,20)

  EndFor
EndFor

lnum=lnum+1
eof="FileEnd"
File.WriteLine(myFile,lnum,eof)
TextWindow.WriteLine("**** txt file has been made. Next pls!")
Goto KeyIn

 

表示が粗くてもとにかく大きなディスプレイが要る際には、こうして作ると便利だと思います。

以上、簡単な記事でしたが皆様のなんらかのお役に立てば幸いです。

 

©2021 Akira Tominaga, All rights reserved.

 

LEDテープを使ったディスプレイの実験(Arduino-Unoで)

f:id:a-tomi:20210802175208j:plain

LEDテープの自由な操作のために、前は速いマイコン(Teensy4.0)を使いましたが、今回はArduino-UnoでLEDテープをディスプレイ代りに使えるかのごく簡単な実験です。よって記事も短いですが。

前回の記事は次です。

カラーLEDテープ照明を自由にコントロール - 勝手な電子工作・・

その後、ネットで次のような密なLEDテープ(の半端1m)を入手しました。

f:id:a-tomi:20210802175622j:plain

ほぼ長さ7mmに1つのRGB-LED (SMD-5050) が並んでいて、コントロール用のIC(WS2812B)もそれぞれに組み込まれているという密なLEDテープです。

こうなると、これでどうしてもDisplayを作ってみたくなってしまいます^^;

しかしそれにはテープを切り崩さなければならないうえ、配線にもちょっとした手間がかかりそう。いい方法がないかなあ・・思っていたら、既にプレート状に配線されている手ごろなのをみつけました。

f:id:a-tomi:20210802180545j:plain

これを並べる方がテープを切って並べるよりは簡単かな?しかし4x4のエレメントではどうにもなりません。そうはいっても数字を出す実験なら最低4x6でできそうだから、モノは試し、やってみよう・・・

というわけで、4つを次のように組み合わせてみます。

f:id:a-tomi:20210802181004j:plain

Arduinoからの信号線は1本。これでもかなり速度の速い表示が可能ですからやってみる価値はありますね。今回はFastLEDライブラリーを使います。

適当につなぎましたが、ややこしいことに次の接続順序になってます。

f:id:a-tomi:20210802181217j:plain

各行を左から右へとつなぐと使いやすいでしょうが、まあ、これでもよいとします。どっちみちソフトでコントロールするわけだから。

というわけで、2桁の数値を表示してみます。どうせならカウントダウン装置にしてみます。最小の4x6を2桁表示すると下に2行あまるわけなので、ここに下線を入れ、残り秒数が少なくなるにつれて下線の色が変わるとか・・

f:id:a-tomi:20210802181654j:plain

LEDがあまりにも明るくて写真が撮りにくいのなんの・・・

というわけできれいな実物と違って、次の動画も周りは暗く映らざるを得ませんが。

youtu.be

LEDの明るさ設定コントロールと撮影のテクニックでもっときれいに撮れるように思いますが、とりあえずはご勘弁くださいm(_ _ )m

Unoのテスト用スケッチは次の通りです。

/********************************************************
*	LED tape panel using FastLED  for Arduini-UNO	*
*	  Test version 00  Aug.1, 2021 by A.Tominaga	*
*********************************************************/
#include "FastLED.h"
#define nLEDs 64            	// number of total LEDs
#define dPin 14             	// data pin D14=A0
CRGB myTp[nLEDs];		// construct myTp array
// define pattern of numeric char
const byte Num[10][3] = {{0x69, 0x99, 0x96}, {0x26, 0x22, 0x27},
  {0x69, 0x12, 0x4F},  {0xF1, 0x21, 0x96}, {0x22, 0x6A, 0xF2},
  {0xF8, 0xE1, 0x1E},  {0x24, 0x8E, 0x9E}, {0xF1, 0x12, 0x48},
  {0x69, 0x69, 0x96},  {0x69, 0x71, 0x24}
};
// define color
#define White (18,18,18)
#define Blue (0,0,24)
#define Green (0,24,0)
#define Yellow (20,20,0)
#define Red (24,0,0)
#define Black (0,0,0)
// for count-down 50 to 0
int8_t Nr = 50;             	// number from 50 to 0
uint8_t lNr, rNr;           	// left & right digit of Nr

void setup() { // ***** Arsuino setup() *****
  Serial.begin(9600);
  delay(2000);
  FastLED.addLeds<WS2812, dPin, GRB>(myTp, nLEDs);
  clrLED();
}

void loop() { // ***** Arduino loop() *****
  lNr = Nr / 10;		// left digit
  rNr = Nr - lNr * 10;		// right digit
  editPat('L', lNr);		// edit left digit
  editPat('R', rNr);		// edit right digit
  drawBar();			// draw under-bar
  FastLED.show();		// and show them
  delay(1000);
  if (Nr==0){			// if counted done
    delay(5000);		// add long delay
  }
  Nr = Nr - 1;			// update number
  if (Nr < 0) {
    Nr = 50;
  }
  clrLED();			// clear LED tape
}

void editPat(byte LR, uint8_t Su) {
  for (uint8_t i = 0; i < 3; i++) {
    byte Byt = Num[Su][i];       // get row i
    for (uint8_t j = 0; j < 8; j++) { // column j
      uint8_t mask = B10000000  >> j;  // set mask
      if (mask & Byt) {         // if Num[Su,bit] on,
        uint8_t k = i * 8 + j;  // then calc. position
        //  if row 1,3,5,.. byte-sequences are opposite
        uint8_t l = k;
        if ((k / 4) % 2 == 1) { // if rows 1,3,5,..
          l = k + 3 - (k % 4) * 2; // reverse sequences
        }
        if (LR == 'L') {        // for left digit
          myTp[l].setRGB White;
        } else {                 // for right digit
          myTp[l + nLEDs / 2].setRGB White;
        }
      }
    }
  }
}

void drawBar(void) {
  for (uint8_t l = 28; l < 64; l++) {
    if ((l < 32) | (l >= 60)) {
      myTp[l].setRGB Blue;
      if (Nr < 40) {
        myTp[l].setRGB Green;
      }
      if (Nr < 30) {
        myTp[l].setRGB Yellow;
      }
      if (Nr < 20) {
        myTp[l].setRGB Yellow;
      }
      if (Nr < 10) {
        myTp[l].setRGB Red;
      }
    }
  }
}
  void clrLED(void) {
    for (int i = 0; i < nLEDs; i++) {
      myTp[i].setRGB(0, 0, 0);
    }
    FastLED.show();
  }

 

エレメントをどんどん増やせば明るくて見やすいきれいなディスプレイができますね!ただ、この状態でもLEDテープは100mAほど消耗しますから、エレメント数を大幅に増やすときは、別電源から供給する必要があります。

 

以上、ごく簡単ですが、どうしてもやってみたかったので^^ ・・・ 簡単ですが記録として残しておきます。

もしもお役にたてば幸いです。

 

©2021 Akira Tominaga, All rights reserved.