![]() |
出荷前連続稼働テスト(DSM501版) |
■発端■
facebook友達のO先生が「PM2.5センサー欲しいけど高い」と書き込んでいて、私が「ポケットマネーで作れますぜ、しかもWiFi対応」とコメントしたところ、受注が決まりました。話が早いw
■仕様確認■
当初は、倉橋屋謹製Board1をベースにしてユニバーサルボードかブレッドボードで、と考えていましたが、「持ち運ぶので丈夫かつコンパクトに」とのことで基板を起こすことになりました。- MCU:ESP-WROOM-02
- 表示器:電子工作のお友達「0.96インチ OLED」
- ダストセンサー:神栄テクノロジー製のPPD 42NJ
- 電源:5vをモバイルバッテリなどから。USBコネクタは使わず変換ケーブル使用
- 3ミリのネジ穴を4つ
- センサー / OLEDはボードに乗せない。
- センサー / OLEDとの接続は逆差し防止の爪付きコネクタ
コネクタの名前がわからなくて、某電子工作コミュニティに質問したら、みなさん教えてくださって、なかには「近くにいるから寄った」と写真まで取ってきてくださった方も。ありがとうございました。いつかコミュニティにお返しができれば良いのですが。
■設計■
Board1 Ver 0.3をベースにがっつりコネクタなどを配置。PPD 42NJについては5v電源・5v出力なので、抵抗で分圧してからESPに入力。
![]() |
ほとんどauto route |
![]() |
はい、絵画的センス皆無ですorz |
■検証■
Board1 Ver.0.0を使い、ブレッドボードでセンサーとディスプレイの動作を確認しました。ここまでだいたい48時間ほど。ホント、話が早いのは良いことです。
■基板発注〜納品■
例によって、最初の設計が出来てから2-3日寝かせます。今回ちょうど週末にかかっているのと他に3種類の基板が同時に設計出来上がったので、いいタイミングでした。
1/18....発注
1/26....深センから発送(EMS)
1/28....到着
1/18....発注
1/26....深センから発送(EMS)
1/28....到着
DHLよりEMSの方が安くて早い。DHLは日本に着いてからが遅いんですよね。
で、週末の1/30、早朝からリフローです。2枚並行して作ろうと思ったのですが、もし設計にミスがあったら2枚分の部品がパーなので、1枚だけ作りました。なお、自分用にリフローする時は、ESPの端子には横一列にまとめてハンダペーストを置いてしまうのですが、今回は受託品なので丁寧に一個一個ランドに置いていきました。そしてホットプレートでリフローし、スルーホール部品をハンダ付けして完成。
とりあえずLチカを…と思ったのですが、LEDを取り付けたIO13にはダストセンサーをつなぐことになっているので、その替わりにTxラインに Serial.write("tarepanda");と送って、ランプが点灯することを確認しました。以前間違えてLEDと直列に10kΩの抵抗を入れたらホタルイカのようにササヤカな光が点ってそれはそれで味わいがありました。
今回は元の設計通り1kΩにしたのですが、明るいわぁ(笑)。あまりにも明るすぎるので納品版は2.2kΩにしました。それでも明るい。
■サンプルソース■
例によって難しいことはしていません。センサーからP1/P2の2本の信号線からパルスが出ていては、埃を検出した時間が長いほどP1/P2のLow時間が長くなるとのことです。P1は1.0μm、P2は2.5μmの埃を検出します。よくあるサンプルだとArduinoのplusIn関数を使ってどちらかのパルスがLowな時間を計測し、それを30秒間合算しているという例が多いのですが、それだとP1/P2の両方を図ることができなくなってしまいます(どちらか一方のパルスに専有されてしまうので)。
なので、このサンプルでは、100μ秒ごとにポートを見に行き、LowかHighを判断し、Lowならカウンタを加算する、という単純な方法をとっています。60個の配列を用意し、各秒ごとに各配列の値を加算すれば、過去60秒間の各秒毎のLow時間を集計できますので、あとは全体を集計すれば過去60秒間の合計Lowを求めることができ、過去60秒間の移動平均も出すことができます。
追記(02/009):ソフトウェアについてもう少し詳しく書きました。
あと、ESP-WROOM-02の作例では、set upで接続されるまで待って、それから処理を継続することが多いのですが、それだとWiFiとの接続ができない場所だとデモもできません。なので、とりあえずbeginで接続を試みたあとは、通常のloopの中で接続成功したか否かを見に行き、結果をOLEDに表示するようにしました。つながらなくてもとりあえず計測および表示を続けていて、つながったら1分ごとにThingSpeak.comへ送っています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ESP_DustCounter and SSD1306 by k.kurahashi 2016-02-05 | |
// | |
// thanks: | |
// OLED Library | |
// https://github.com/squix78/esp8266-oled-ssd1306 | |
// | |
// ThingSpeak.com | |
// https://thingspeak.com/channels/81094 | |
// | |
#define USE_US_TIMER 1 | |
extern "C" { | |
#include "user_interface.h" | |
#include "osapi.h" | |
} | |
#include <Wire.h> | |
#include <Time.h> | |
#include "SSD1306.h" | |
#include "SSD1306Ui.h" | |
#include <ESP8266WiFi.h> | |
#include <WiFiClient.h> | |
// | |
// OLED / SSD1306 | |
// | |
SSD1306 display(0x3c, 4, 5); | |
// | |
// Wifi | |
// | |
const char *ssid = "*************"; | |
const char *password = "*************"; | |
WiFiClient client; | |
// | |
// dust counter | |
// | |
const int DustCounterP1 = 14; | |
const int DustCounterP2 = 13; | |
ETSTimer Timer; | |
int prevSecond = second(); // 秒ごとに表示更新するためのフラグ | |
int prevMinute = minute(); // 分ごとに計測・送信するためのフラグ | |
static long sum1, current1; | |
static long sum2, current2; | |
static long store1[60], store2[60]; // data ring buffer | |
static float p1, p2; // 直近の計測値(1分移動平均) | |
static long c1, c2; // 直近の計測値(直近1秒) | |
// | |
// setup | |
// | |
void setup() { | |
system_timer_reinit(); // to use os_timer_arm_us | |
Serial.begin(115200); | |
Serial.println(); | |
Serial.println(); | |
// OLED | |
Wire.begin(); | |
display.init(); | |
display.flipScreenVertically(); | |
display.displayOn(); | |
display.clear(); | |
display.setFont(ArialMT_Plain_10); | |
display.setTextAlignment(TEXT_ALIGN_LEFT); | |
display.drawString(0, 0, "status"); | |
display.setTextAlignment(TEXT_ALIGN_RIGHT); | |
display.drawString(127, 0, "started"); | |
display.display(); | |
// | |
// Wifi | |
// | |
WiFi.begin ( ssid, password ); | |
Serial.println("connecting..."); | |
// Wait for connection | |
while ( WiFi.status() != WL_CONNECTED ) { | |
delay ( 500 ); | |
Serial.print ( "." ); | |
break; | |
} | |
Serial.println("Wifi Connected"); | |
// | |
// dust counter | |
// | |
sum1 = sum2 = current1 = current2 = -1; | |
for (int i = 0; i < 60; i++) { | |
store1[i] = store2[i] = -1; | |
} | |
pinMode(DustCounterP1, INPUT); | |
pinMode(DustCounterP2, INPUT); | |
os_timer_setfn(&Timer, fetchFunction, NULL); | |
os_timer_arm_us(&Timer, 1000, true); | |
} | |
// | |
// loop | |
// | |
int dispUpdate = second(); | |
void loop() { | |
char buf1[10], buf2[10]; | |
// 1秒ごとに表示を更新する | |
if (dispUpdate != second()) { | |
dtostrf(p1, 7, 2, buf1); // 直近の計測値を文字列に | |
dtostrf(p2, 7, 2, buf2); | |
// OLEDへ表示 | |
display.clear(); | |
display.setFont(ArialMT_Plain_10); | |
display.setTextAlignment(TEXT_ALIGN_RIGHT); | |
display.drawString(48, 0, "status"); | |
display.drawString(48, 16, "P1"); | |
display.drawString(48, 32, "P2"); | |
display.drawString(48, 48, "raw cnt"); | |
display.setTextAlignment(TEXT_ALIGN_RIGHT); | |
// WiFiの接続状態を表示 | |
if (WiFi.status() == WL_CONNECTED) { | |
display.drawString(127, 0, "connected"); | |
} else { | |
display.drawString(127, 0, "unlink"); | |
} | |
display.setFont(ArialMT_Plain_16); | |
display.drawString(127, 12, buf1); | |
display.drawString(127, 28, buf2); | |
// 直近の計測値(1秒間のカウント)を表示 | |
if (c1 != -1) { | |
char bufCurrent[20]; | |
sprintf(bufCurrent, "%5d/%5d", c1, c2); | |
display.setFont(ArialMT_Plain_10); | |
display.drawString(127, 48, bufCurrent); | |
} | |
display.display(); | |
dispUpdate = second(); | |
} | |
// 1分ごとにThingSpeak.comへ送信 | |
if (prevMinute != minute()) { | |
if (WiFi.status() == WL_CONNECTED) { | |
send(buf1, buf2); | |
} | |
prevMinute = minute(); | |
} | |
delay(1000); | |
} | |
// | |
// Lowレベルの比率をダストカウント量に変換 | |
// y = 1.1x^3 - 3.8x^2 + 520x + 0.62 | |
// この式は以下のサイトの近似式を流用させていただきました。ありがとうございます。 | |
// thank you for this site > http://www.howmuchsnow.com/arduino/airquality/grovedust/ | |
// | |
float conversion(float inRate) { | |
return 1.1*pow(inRate, 3.0) - 3.8*pow(inRate, 2.0) + 520.0*inRate + 0.62; | |
} | |
// | |
// 100uSごとにP1とP2を監視し、Lowレベルの時間を計測する | |
// DustCounter check both ports every 100uSec to detect LOW level duration | |
// | |
void fetchFunction(void *temp) { | |
// 1秒ごとにリングバッファの計測スロットをずらしていく | |
int tempSecond = second(); | |
if (tempSecond != prevSecond) { | |
Serial.println(tempSecond); | |
int n = 0; | |
long sum1 = sum2 = 0; | |
for (int i = 0; i < 60; i++) { | |
if (store1[i] != -1) { | |
sum1 += store1[i]; | |
sum2 += store2[i]; | |
n++; | |
} | |
} | |
p1 = conversion((float)sum1 / (float)n / 1000.0); // Lowレベル比率をダスト量に換算 | |
p2 = conversion((float)sum2 / (float)n / 1000.0); | |
c1 = store1[prevSecond]; // 直近のカウント率を取得 | |
c2 = store2[prevSecond]; | |
store1[tempSecond] = 0; // 次のカウント記録場所をクリア | |
store2[tempSecond] = 0; | |
prevSecond = tempSecond; // 秒監視フラグをクリア | |
} | |
// P1がlowなら、P1のカウンタをインクリメント | |
if (store1[tempSecond] != -1 && digitalRead(DustCounterP1) == LOW) { | |
store1[tempSecond]++; | |
} | |
// P2がlowなら、P2のカウンタをインクリメント | |
if (store2[tempSecond] != -1 && digitalRead(DustCounterP2) == LOW) { | |
store2[tempSecond]++; | |
} | |
} | |
// | |
// ThingSpeakへ送信 | |
// | |
void send(char *buf1, char *buf2) { | |
String postStr = "&field1=" + String(buf1) + "&field2=" + String(buf2); | |
String apiKey = "*******************"; | |
Serial.print("Connecting..."); | |
if (client.connect("184.106.153.149", 80)) { // api.thingspeak.com | |
Serial.print("Connected...."); | |
postStr = apiKey + postStr + "\r\n\r\n"; | |
client.print("POST /update HTTP/1.1\n"); | |
client.print("Host: api.thingspeak.com\n"); | |
client.print("Connection: close\n"); | |
client.print("X-THINGSPEAKAPIKEY: " + apiKey + "\n"); | |
client.print("Content-Type: application/x-www-form-urlencoded\n"); | |
client.print("Content-Length: "); | |
client.print(postStr.length()); | |
client.print("\n\n"); | |
client.print(postStr); | |
Serial.println("posted."); | |
} | |
client.stop(); | |
} |
■公開情報■
githubにてeagleデータを公開準備中です。ソースとEagleのデータをひとまとめにして公開しようとしているのですが、ついでに自分の作業用ディレクトリも大整理しているところなので時間がかかってます。
■ビルド方法など■
長いことArduino / ESPを使っていると、いつの間にかいろいろなライブラリが充実しているのですが、「これが最初!」ということになると素直に走ってくれません。というわけで、ビルドに必要な方法を挙げて行きます。- USBシリアルのドライバをインストール
- Windowsの場合は、お使いのUSBシリアルのドライバをインストールするだけ
- MacOS Xの場合、10.8以降、ドライバに認証が必要になったため、ややこしくなってしまいました。
- Arduino IDE 1.6.5
- とりあえず動作確認した環境は1.6.5です。
- 公式ページからダウンロードしてインストールしてください。
- OLEDライブラリ
- 以下のgithubからzipをダウンロードします。
- Arduino IDEを起動し、スケッチ>Include Library...>Add .ZIP Libraryで先ほどダウンロードしたOLEDライブラリを指定して読み込んでください。
- Time.h
- 時間を扱うためのライブラリに含まれます。
- このページの「Download ZIP」をクリックしてダウンロードしたファイルを上記と同じ手順で読み込みます(スケッチ>Include Libraryうんぬん
■謝辞■
各ライブラリの作者の皆様、
そして、この基板の発注主であり記事として公開することを快く許してくださったO先生、
ありがとうございました。
ご参考までに今回のオーダーメイド完成品で1セット2万円以下です(完成品1個+未使用基板8枚、デモ用ソフトと送料)。
また、5台からElecrowにて量産可能です。その場合、今回のものなら外付け部品も全部そろえて10台5万円程度です。
オーダの流れ:
■お問い合わせ■
ご購入や製品開発についてのご相談はこちらまでお気軽に。ご参考までに今回のオーダーメイド完成品で
また、5台からElecrowにて量産可能です。その場合、今回のものなら外付け部品も全部そろえて10台5万円程度です。
- メールで「こんな機能が欲しい」とご相談ください。
- 何度かのやりとりの後、構成案と費用をお知らせします。
- PayPalにてお支払いください。
- PayPalはショッピングプロテクションがあり、もし納品されない場合には返金されるので安心です。私もebayで何度かお世話になりました。
- 試作、基板設計、基板発注、基板完成、実装…など各段階で進捗をお知らせします。
- 宅配便にて完成品をお送りします。
- Google+にてユーザサポートを承ります。
- グループでのサポートは無償です
- privateサポートについてはご相談ください。
□追記(2018年12月14日)□
経験値上昇の伴い、値上げしました。最低4万円からですm(_ _)m。
あと、お取引にはココナラハンドメイドを使いますので、契約時にクレジットカード等でココナラに対して支払っていただき、納品後に私に対して決済される形になります。
ご相談はこちらまで。お気軽にどぞ。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。