自分用のmyJJY(正確な時刻をそっとモールスで知らせる)
ディジタル情報が氾濫していますが、あまり良いニュースもない昨今ですね。そのうえ暑さ続きで、しばらくの間はエアコンが頼りでした。こういう時は自分のやるべきことに邁進するのがよい感じですね。
そうしているうち、作業に没頭し時間を忘れてしまうような癖がついてきたのに気づきました。結果は当然寝不足の毎日^^; これを何とかしようと考え、自分用に定期的に時刻をそっと告げる装置があると良いだろうなと思い始めました。
JJY受信して簡単にできるつもりでしたが、現代ではそうはいかなくなっています。様々な電波が飛び交う今、工作室内でJJY(当地では40kHzAM)を正確に受信できることは稀になってきたのに気づきます。受信できた時だけRTC(リアルタイムクロック)を補正するという手もありますが、それでは普通の時計であって余り面白くはない感じ。この際は、今や受信しやすくなったGPS(GNSS)信号で行うことにしました。
時刻と位置をマイコン1(MC1)で取り出し、OLEDに表示しつつ、報知する間隔の時刻直前で別のマイコン2(MC2)にI2Cで時刻を伝え、そこからできるだけ優しく感じる正確な時報を出して伝える装置をと考えました。

マイコン2(MC2)はI2Cスレーブとし、時刻をそこで心地よい音程のモールスに置き換えて、加えて正確な時報を鳴らします。モールス音は440Hz正弦波にし、クリック音がまじらないよう、断続を正確にゼロクロス点で行います。そのため、シンセサイザーICを使うことにし、実際に優しい音になりました。もちろん時報寸前だけはクリック音にしてますが。ローパスフィルタ経由でディジタルアンプを通して、とりあえずは百均スピーカに出しています。
さて、こういうのは凝りだすときりがありません。作り始めたらあれこれ気づいて、またもや没頭してしまいます。時間がもったいないのに逆効果は避けたいところでした。というわけで、まずは試作のままで板の上に載せて使い始めました。時間を守るのに効果があれば、なるべくならレトロ風の本番機を作ることにします。
・・・
と書いていたら、試験装置から深夜の合図です。続きは今度時間のある時に書くとします。
2025年10月26日 製作・使用状況を少し追記
効果絶大なので、毎週ごく僅かずつですが組み立ててきました。骨組みはできたのでそのまま使用中。次の写真の状態で、少しずつケースを製作中。仕事に没頭して明け方になったりするのを防ぐ、とにかく私には役立つ道具です・・・。
机上の面積が狭くて済むよう6㎝x6cmの基板を3枚使い、4階建て高さ12cmの小さな建物のようにしました。全体のケースは少しずつ製作中。

右下にまだヤスリ掛け不十分な個所もあります^^; 次にバラした時に直します。
1階はスピーカーと電源関連。2階は時報寸前に信号を受け、シンセサイザーでモールス信号と時報を出力。音量調整月ディジタルアンプがあります。3階はGPS衛星信号を解析し1.3インチの小型OLEDに表示。そして時報用信号を2階のマイコンにI2Cデータとして送ります。4階にはGPSアンテナだけを収容しています。
天窓のある部屋とはいえ、室内なのに衛星を常時8~11個ぐらい捕捉しています。アンテナの下にある基板のべたアース等が回路のEMIを遠ざけたせいか、試作段階より受信がずっと良好です。そして、同一場所に置いている場合は緯度経度が100万分の1度単位でほぼ固定され、場所が10cm単位で分かってしまいます。(部屋内のどこに置いたかまでわかります。)そのため写真の座標情報の一部にモザイクをかけます^^;
これの名前を「myJJY」としました。音声信号は構想時点から少しだけ変更し、モールスで①JOKE(単なる冗談局艸)、②正確な時報、③QTR(Q符号で時刻通報という意味) ④時分(略式数字で短いが正式)。⑤終わり符号のAR。
今後主にアクリル板で壁を仕上げ、スピーカー前には古風なサランネットか何かを取付ける予定です。この忙しさでいつ出来上がるかは・・。とはいえ自分用なので、慌てず時間のあるときだけ、少しずつコツコツと完成します。
そういう傍から時報で、あれ、こんな時間に!!
11月4日追記:
先日完成しました。小さな装置ですが、机の隅で30分単位に絶対に狂わない正確な時刻をきれいな時報とモールス音でそっと知らせてくれます。これで深夜に気づかず作業に没頭し続けるようなことが解消されています。まあとにかくどんどん忙しくなる世の中ですからね。
ご用心 技術がもたらす 繁忙感 | パーソナルPM Lessons Learned
まずは構造の入れ物ですが、ケースは2mm厚の透明アクリルだけでがっちり作成しました。アクリル板にCNCルーターで描いた図が次です。黄色部分は基板カッターでカットの後バンドソーで切り落とし、次に各部品の必要箇所をアクリルヒーターで内側へ直角に曲げます。また、穴あけ部分はドリルの中心穴だけをしるすようにしています。つまりドリルの樹脂ビットの先端位置だけにしています。こうしないと穴がずれるため。

裏ブタは垂直取付けブロック(秋月で売ってます)を介して、ケース本体とねじ付けです。完成したのが次。自分用なので多少雑でして、1.3インチOLEDのフィルムもまだはずしておらず、多少ゴミも見えます。次回に開けた時に掃除します ^^;

衛星を大体いつも8個以上捕捉しているので、設置位置を変えなければ緯度経度は100万分の1度の桁までが大体固定されます。つまり10cm単位で設置場所がわかるため、途中の3桁をぼかさせていただきました(そういえば、これはだいぶ前に書いた事でしたね)。
回路は次のとおりです。

回路は単純ではありますが、内部のEMI等には少し気を付ける必要があります。アンテナはRFIなどを受けないように構造の一番上に設置し、U1とU2は3Fに、U3~U5を2Fにまとめ、スピーカーと電源関連を1Fに問分けてあります。これにより、そういう問題は出にくいと思います。U2がI2CのマスターでU3はI2Cのスレーブで、400kHz交信なので、本来はノイズを出しやすいものと思います。
とりあえずここまでを追加し、音声やプログラムの内容等はまた次回に時間ができたときに追加したいと思います。
以上、すこしずつの追記ですみません が、、<(_ _)>
©2025 Akira Tominaga, All rights reserved.
超音波定在波が目で見えるようになってきました(8/8更に追記)

前から気になっていた超音波定在波の実験を、他の目的に役立てるために短時間でやってみました。長さ7㎝ほどの小さく安価なキットを使いましたが、これがなかなかの優れものです。

発泡スチロールの小粒を空中に浮かせる「超音波浮遊」装置キットの構造はかなり単純です。超音波スピーカー2個を一定距離で向かい合わせにして、両方から同一周波数同一位相の超音波を出して間に定在波を作ることで、音圧が最も低い箇所に粒が浮いて固定されるというわけです。
スピーカー片側の取付け角度が少し悪かったのですが、それでも粒はちゃんと浮きます。この回路は次のような構成です。

超音波発生用信号の周波数は、オシロスコープで見ると40.1kHz程度です。スピーカがそういう仕様ですから推測どおりですが。こういう電気信号をオシロで測る際は、用心のために最初はプローブをグランド以外にはつながずに見るに限りますね。万一のサージなどでオシロを壊したくないのと、それだけで目的が十分達成できることが多いからです(対象の出力インピーダンスの低さ次第)。

両スピーカー間の物理的な距離は22㎜にみえますが、実際はその表面から4㎜奥に振動板部があるので、正確には30㎜ぐらいになっています(測り直して修正しました)。
この装置では粒が安定する場所が3か所あります。装置全体を横にしたり反転したりしても、粒はそう簡単には落ちません。どういう定在波なのかと知りたくなります。実は定在波等表示用のデジタル・シュリーレン観察器を工夫中で、その試験対象にするため作ったわけでした。本体の観察器はまだできていません。(8/8追記:ついに観察できるようになりました。この記事の最後に書いておきます。)
とはいっても、どうしてもそれを今知りたくなるわけで、この際は簡単なシミュレーションをしてみます。この条件だと音圧の深い谷が3層できており、その場所からは粒が簡単には出ていけないことが分かります。スピーカーの振動板の位置を赤で示します。(8/8更新:温度の影響がわかるようにV02に更新しました。)

これを出力したPythonプログラムを次につけておきます。音波の周波数とスピーカー間距離、そして空気の温度を、画面上のスライダーで変更できるように作ってあります。このプログラムはGoogle Colabで実行します。
# Simulation of standing waves of Ultrasonic Levitation
# Initial version V01 June 21, 2025 by Akira Tominaga
# V02 Reflects temperature changes. Aug.5,2025
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, FloatSlider
from google.colab import output
output.enable_custom_widget_manager() # just in case
# --- setup Grid ---
x = np.linspace(-0.012, 0.012, 400) # Horizontal ±1.2 cm
y = np.linspace(-0.02, 0.02, 600) # Vertical ±2 cm
X, Y = np.meshgrid(x, y)
# --- Plotting function with interactive sliders ---
def plot_wave(distance_cm, frequency_kHz, tempC):
f = frequency_kHz * 1000 # Convert kHz to Hz
c = 331.5 + tempC * 0.6 # sound speed [m/s]
λ = c / f # wave length
k = 2 * np.pi / λ # phase of sound
d = distance_cm / 100 # Convert cm to m
speaker_y1 = -d / 2
speaker_y2 = d / 2
r1 = np.sqrt((Y - speaker_y1)**2 + X**2)
r2 = np.sqrt((Y - speaker_y2)**2 + X**2)
P = np.sin(k * r1) + np.sin(k * r2) # sound pressure
membrane_x = np.linspace(-0.002, 0.002, 50)
plt.figure(figsize=(5, 6.5))
contour = plt.contourf(X * 100, Y * 100, P, levels=50, cmap='viridis')
plt.colorbar(contour, label='Sound pressure (normalized)')
plt.plot(membrane_x * 100, [speaker_y1 * 100] * len(membrane_x), color='red', lw=5)
plt.plot(membrane_x * 100, [speaker_y2 * 100] * len(membrane_x), color='red', lw=5)
plt.title(f'Standing Wave\nDistance = {distance_cm:.1f} cm, Frequency = {frequency_kHz:.2f} kHz, {tempC} ℃')
plt.xlabel('Horizontal position [cm]')
plt.ylabel('Vertical position [cm]')
plt.axis([-1.2, 1.2, -2, 2])
plt.tight_layout()
plt.show()
# --- Interactive UI with extended frequency range ---
interact(
plot_wave,
distance_cm=FloatSlider(value=3.0, min=2.0, max=4.5, step=0.1, description='Dist[cm]'),
frequency_kHz=FloatSlider(value=40.1, min=38.0, max=46.0, step=0.1, description='Freq[kHz]'),
tempC=FloatSlider(value=20,min=10, max=50, step=1, description='Temp[C]')
);
とても短い紹介でしたが、何かのお役に立てば幸い。
では今回はこのへんで。
====================
2025.8.08 追記を更新
本来の目的であるオリジナル版ディジタルシュリーレン装置の改良を続けています。必須のハードウェアは4K程度のWebカメラ1台だけです。気体の屈折率の微妙な違いによる、虚像の見える方角の僅かなずれを、オリジナルのプログラムで解析して結果を表示するしくみです。
一番最初の版の出力はこういうものでした。

流体の屈折率を変えるための熱源を小さな点1つだけにしたまま、しばらくプログラムの改良を少しずつ重ねてきました。

原理は、画像データを2560x1440(約370万)の要素に分割して、微熱流を流す前後での各要素のブレを高速で計算します。写る虚像が縦方向にずれる方向と量で色表示をしていたのですが、目的が流れの観察だけなので、途中から符号を外して絶対量だけに変えています。
だいぶ安定して観察できるようになったので、今回、熱源を点ではなく線にしてみました。独自の工夫で、画面と平行な薄い平面のような気流を流しています。下の写真では左側から右方向へと流します。

簡単にしつらえたため上の方にこの超音波装置の電源線や取り付け具などが見えていますが、実験なのでご容赦ください。線状の熱源はニクロム線を細いコイルにした簡単な手作りで、今のところは熱が等分布ではないため流れに少しまだらがある状態です。それでもとにかくまずはやってみるわけです。
次が、左から右へとゆっくり動く薄い平面状の気流です。つまり気体の粗密を示す断面図のような出力画像です。

これで超音波の定在波の強い粗領域3か所がバッチリ見えているじゃありませんか!! 気流によって少し右へ偏っているのは致し方ないとしまして。なお、ブレ方向が反転する境目が出ているものですが、流れでなく定在波を見る場合は、以前のように量と符号でカラーを変える方が見やすいのかもしれません。今後じっくり検討しやってみます。
とにかくこれで大いに気をよくし、この小さなディジタルシュリーレン装置を更に改良していくわけです^^
8月8日、さらに改善してきたので追記します。
まずは、測定箇所周囲の風防を大きくして、より精密に観察できるようにしました。また、カメラ視点での虚像のブレの上下方向も示せる表示プログラムに直しました。
次のぶれの絶対値表示ですと、精密になったぶん混み入って分かり難いわけです。

そこで、絶対値表示に加えて符号つきで観察できるように直しました。虚像を見る歪が下(負)にブレるのを青、上(正)にブレるのを赤で表示したのが次です。

空気の疎部分(つまり定在波の音圧が低い箇所)の塊は凹レンズの働きをしますから、虚像のブレは上に青の層、下に赤の層のペアとなります。つまり、上から青→赤と重なるペアが確認したい定在波の疎の部分です。上の写真では発泡スチロールが浮遊した場所に、その3ペアがあるのがわかります。これでようやく納得できるものになってきましたが、さらに改良を続けてから完成したいと思います。
現時点では手作りの熱源部品がやや荒っぽいので、次はこれを改善することでさらに鮮明にしようなどと考えています。時間のあるときに限られますが、最終的には可搬型の扱い易い装置にしたいもの。
以上、追加でした。
©2025 Akira Tominaga, All rights reserved.
超便利なI2C-IOエキスパンダ、TM1637LEDを複数接続など
ここ数年、色々なオリジナル装置を考えては作っています。どれも仕事がらみの製作であるために、投稿ができないでおります。
読者の皆様に申し訳ないなと思いますがついつい長らくご無沙汰でした <(_ _)>。今回はお詫びのしるしで、凄く短い記事です(・・?)。
今、真空関連の制御装置をこしらえていますが、その表示には少し離れても見やすいLEDディスプレイを複数使うことにしています。TM1637ディスプレイは普通は表示が楽ですが、独自プロトコルの専用線を2本使う装置であるため、複数個つけると使うピンが増えるし、またソフトの上では多少の工夫が要るものです。今回は、他に押しボタンも複数必要なため、マイコンのピン数節減のためのIOエキスパンダでつないでみました。
さきほど実験をしたところなのですが、表示速度も十分に出せ、これは便利だなと思いました。ここではその使い方をごく簡単に紹介し、最後にArduino IDEスケッチ例を添付します。このエキスパンダをお使いの方は多いと思いますが、TM1637表示装置の専用ライブラリはエキスパンダに対応しませんし、海外サイトにも使用例がありません。しかしプロトコルは単純なのでライブラリーなしで動かします。(その方法等については次をご参照ください:TM1637-LEDをライブラリーなしで簡単表示 - 勝手な電子工作・・)
このエキスパンダ1つでTM1637だけなら4個まで使えるし、エキスパンダ自体もI2Cに8個までつけられます。マイコンならほぼどれでもこのエキスパンダが使えるのを確認しました。
TM1637との通信をI2Cに合わせて100kHzや400kHzにしたい場合などに、マイコンの速度が多少速いほうが楽です。ここではESP32-WROOM32での例を示します。なお遅いマイコンでも、添付プログラム中のTM1637への信号速度を決める定数(例では#define lTu=5μ秒)を25μ秒などに遅くすれば使えます(Uno R3で確認済です)。
エキスパンダはTI社の”PCF8574 Remote 8-bit I/O Expander for I2C Bus”で16ピンの小さなICですが、パッケージは4種類でDIPもあるので実験・実装とも楽です。開発対象ではボタンスイッチ複数も併せて使うので、ここではTM1637ディスプレイを2台とスイッチを4個つないで実験しています。
エキスパンダの仕様や使い方は他の多数のサイトや、PCF8574のデータシートにも分かり易く書かれています。ここでは全て省略させていただき、スケッチを見ていただく方が早いと思います。
/* *********************************************************
Dual TM1637s & Buttons via PCF8574 - sample for ESP32 -V00
Function: Output to dual TM1637 displays, and input from
4 button switches, with only I2C SCL and SDA.
Original version July 23, 2024 (c) Akira Tominaga
* ********************************************************/
// *** for PCF8574 I2C IO expander(Texas Instrument)
#include "PCF8574.h"
PCF8574 PCF(0x20); // name PCF and I2C addr 0x20
byte Bs; // status of buttons
// *** for Dual TM1637 LED displays(connect to PCF8574 pins)
const uint8_t Clk0 = 4; // CLK0 pin for TM0
const uint8_t Clk1 = 6; // CLK1 pin for TM1
const uint8_t Dio0 = 5; // DIO0 pin for TM0
const uint8_t Dio1 = 7; // DIO1 pin for TM1
#define TM0 0 // TM1637 Display #0
#define TM1 1 // TM1637 Display #1
#define lBrt 0x03 // duty-rate 1/16x 02:4,03:10,05:12,07:14
#define lTu 5 // LED line time unit in micro-second
byte lChr; // working byte for sending to LED TM1637
uint8_t LEDini0[] = { Clk0, Dio0, 0x08, 0x08, 0x08, 0x08 };
uint8_t LEDini1[] = { Clk1, Dio1, 0x08, 0x08, 0x08, 0x08 };
uint8_t D[2][6]; // Data matrix 2x6bytes (Clk, Dio, datax4)
#define Clk 0 // Clk position in D[][]
#define Dio 1 // Dio position in D[][]
#define Dtop 2 // Data top in D[][]uint
#define Dend 5 // Data end in D[][]
void setup() { // ***** setup *****
Serial.begin(115200);
Wire.begin();
PCF.begin(); // pull-up SCL & SDA, and Write 0xFF for P0-7
// *** set Data Matrix with initial 8888
for (int dN = 0; dN < sizeof(LEDini0); dN++) {
D[TM0][dN] = LEDini0[dN];
D[TM1][dN] = LEDini1[dN];
}
for (int tmN = 0; tmN < 2; tmN++) {
PCF.write(D[tmN][Clk], HIGH);
PCF.write(D[tmN][Dio], HIGH);
lDispData(tmN);
}
delay(1000);
}
void loop() { // ****** Loop *****
// *** read status of buttons and take required proc.
Bs = 0xFF-PCF.read8(); // read complement of 8bits
if (Bs != 0x00){ // if not all L (H complement)
Serial.println(Bs,HEX); // show them (for test only)
}
// *** set numbers to D matrix and show them on TM1637 LEDs
for (int tmN = TM0; tmN <= TM1; tmN++) {
for (uint8_t dC = Dtop; dC <= Dend; dC++) {
D[tmN][dC] = random(0, 9); // set random num
}
LZsup(tmN); // supress left zeros
lDispData(tmN); // and display data
}
delay(1000);
}
/**************************************
User defined functions
* *********************************** */
// *** lDispData(tmN) *** display data to 4dig LED
void lDispData(int tmN) {
#define showDat 0x40 // show TM that this is data
#define showAd0 0xC0 // LED data addr is zero
#define showDcB 0x88 + lBrt // show dCtl + brightness
lStart(tmN); // start signal for tmN
lChr = showDat; // identifier for data
lSend(tmN); // send it
lStop(tmN); // stop signal
lStart(tmN); // and restart
lChr = showAd0; // identifier for address
lSend(tmN); // send it
// *** data conversion to LED images
for (int j = Dtop; j <= Dend; j++) { // from D[][2] to D[][5]
byte edChr = D[tmN][j]; // set a byte to be edited
switch (edChr) {
case 0x00: lChr = 0x3F; break; // 0
case 0x01: lChr = 0x06; break; // 1
case 0x02: lChr = 0x5B; break; // 2
case 0x03: lChr = 0x4F; break; // 3
case 0x04: lChr = 0x66; break; // 4
case 0x05: lChr = 0x6D; break; // 5
case 0x06: lChr = 0x7D; break; // 6
case 0x07: lChr = 0x07; break; // 7
case 0x08: lChr = 0x7F; break; // 8
case 0x09: lChr = 0x6F; break; // 9
case 0x0A: lChr = 0x77; break; // A
case 0x0B: lChr = 0x7C; break; // b
case 0x0C: lChr = 0x39; break; // C
case 0x0D: lChr = 0x5E; break; // d
case 0x0E: lChr = 0x79; break; // E
case 0x0F: lChr = 0x71; break; // F
case 0x10: lChr = 0x00; break; // blank
default: lChr = 0x7B; // e for error
}
lSend(tmN); // send each byte (lChr) to tmN
} // end of four bytes
// *** last half protocols
lStop(tmN); // stop signal
lStart(tmN); // restart
lChr = showDcB; // display brightness
lSend(tmN); // send it to tmN
lStop(tmN); // stop signal to tmN
}
// *** LZsup(tmN) *** left zero suppression in Data
void LZsup(uint8_t tmN) {
#define Blank 0x10 //code for LED blank
if (D[tmN][Dtop] == 0) { // if Dtop = 0
D[tmN][Dtop] = Blank; // set blank value
if (D[tmN][Dtop + 1] == 0) { // if also Dtop+1 = 0
D[tmN][Dtop + 1] = Blank; // set blank value
if (D[tmN][Dtop + 2] == 0) { // if also Dtop+2 = 0
D[tmN][Dtop + 2] == Blank; // set blank value
}
}
}
}
// *** lSend *** send lChr
void lSend(int tmN) {
#define LSb B00000001 // from LSB
for (int bit = 0; bit < 8; bit++) { // for 8bits
if ((lChr & LSb) == LSb) { // if one then
PCF.write(D[tmN][Dio], HIGH); // set DIO high
} else { // else
PCF.write(D[tmN][Dio], LOW); // set DIO low
}
// *** clock on to show signal
PCF.write(D[tmN][Clk], HIGH); // set CLK HIGH
delayMicroseconds(lTu);
PCF.write(D[tmN][Clk], LOW); // set CLK LOW
delayMicroseconds(lTu);
lChr = lChr >> 1; // shift bits to right
}
// when all 8bits done, simulate receiving Ack
PCF.write(D[tmN][Dio], LOW); // DIO L during Ack
// *** clock on for LOW signal
PCF.write(D[tmN][Clk], HIGH); // set CLK HIGH
delayMicroseconds(lTu);
PCF.write(D[tmN][Clk], LOW); // set CLK LOW
delayMicroseconds(lTu);
}
// *** lStart *** send start signal
void lStart(int tmN) {
PCF.write(D[tmN][Dio], LOW); // set DIO HIGH
delayMicroseconds(lTu);
PCF.write(D[tmN][Clk], LOW); // set CLK HIGH
delayMicroseconds(lTu);
}
// *** lStop *** send stop signal
void lStop(int tmN) {
PCF.write(D[tmN][Clk], HIGH); // set CLK HIGH
delayMicroseconds(lTu);
PCF.write(D[tmN][Dio], HIGH); // set DIO HIGH
delayMicroseconds(lTu);
}
/* End of program */
©2024 Akira Tominaga, All rights reserved.
室内空気の微小粒子状物質濃度記録計ーはんだ付けがこんなに(+_+)?

小さな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)
この季節は天気予報が頻繁に変わるのに気づきます。PCやスマホなどで見れば豊富に出てくるわけですが、この際は、常時表示してくれる小さな予報器が手元に欲しくなりました^^; 今回はその試作記事を書きます。
筆者はこのところ省エネ型土壌水分(pF)計などに没頭中なのですが、出せる内容の開陳は少し先になりそう。そこで、このブログの記事を長らくさぼってしまってすみませんm(_ _ )m。久々に時間の取れる週末が来たので、気分転換も兼ね、天気予報器作りに早速トライをしました。
自分で気象観測して予報してもあまり当たりません(・・? 。ですからもちろんWebのオープンデータを使います^^; ので、逆にすぐできそうです。ネットを少し調べた限りは、世界の地域とデータを広くカバーするOpenWeatherMapの個人用APIを使うのがよさそうです。常に更新されています。
使い方はここで書くより、https://openweathermap.org/guideを直接見てみていただくのが早くて正確だと思います。丁寧に書かれていますし、ブラウザーの翻訳などで和訳しても読み易い感じですから。

上は画像なので赤字のリンクは効きません、リンク先で直接ごらんくださいね。
おおざっぱに言えば、まず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”を、自分の環境に更新したものを同居させます(もしそうしない場合、他のデバイスを使うたびにライブラリー内のこれらを変更することになり、既に作ったものを使うときに改めて戻したりする必要がありそうですから)。

冒頭の写真のように、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 ***

以上、ごく簡単な紹介でしたが、もし皆様の何らかのお役に立てばとても幸いです。
©2022 Akira Tominaga, All rights reserved.
ロードセルで精密秤(簡単・正確、コスパも抜群)

趣味で土壌水分のデジタル負圧測定に取組中ですが、そちらは試作までに期間がかかりそうです。息抜きにストレーンゲージのデジタル測定をしてみました。最近の測定精度やコスパの向上はどんな感じかと・・。
やってみたら、ずいぶん簡単・正確・低コストになったものです!精密な測定が今や簡単、0.1グラム単位の秤などがたやすくできます。以下、これを紹介したいと思います。0.01グラム単位でもできそうです。
昔からあるストレーンゲージですがゲージ自体の改善はもちろん、測定がデジタル化でおおいに進歩。ただ、僅かな伸び縮みを検出する抵抗なので直接扱うとそれなりに手間がかかります。エポキシ等での接着も、細いリードの扱いなども。また場合によっては、熱膨張率によるごく僅かな「見かけの歪」を防ぐため貼付対象に合うゲージ選択(1例:アルミ系なら約24ppm/℃、鉄系なら12ppm/℃)など。伸びる側と縮む側各2か所ずつ計4枚でホイートストーンブリッジにして測ります。
手持ちの1例は次で、7.5 x 4mmほどの大きさ(写真下に1mmの目盛り)で抵抗値は1000Ωでアルミ系材料用。基本型は同じものを4枚使ってフルブリッジ回路にします。基本型用以外に目的に応じパターンが複数あるものや45度のもの、より小さなゲージなどもあります。

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

精密なAD変換やその温度補正など、頼れるこのらくちんなモジュールは、海外ネットでは僅か50円台で売られています!これを使わない手はありませんね。
ネットで探すと貼る相手の材料(ロードセル)も数十円からあります。さらに「ストレーンゲージ貼付済のロードセル + HX711モジュール」で、な、な、なんと百数十円で売られています。それは助かるなあ^^ これならすぐできて相当楽でしょう。

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

1kg用を求めましたが、今回のテスト結果からみると10kg用などの丈夫なものであっても、測る単位が0.1gなら十分精密に測れそうです。
入手したゲージ貼付済ロードセルは次です。


中心辺りに双眼鏡型の空洞があり、「ダブルベンディングビーム型ロードセル」とか、「バイノキュラーデュアルビーム型ロードセル」と呼ばれるようです。
ところで、この空洞によって、単なる直方体の単一ビームとは違う伸び縮みをします。力によるひずみを大げさに描けば次の図のようになると思います。手書きですみませんが。

下の形だとPに荷重がかかったときに沈む方向はまっすぐ下です。というより、P点付近の天端は傾かず平行移動し、片持ち梁のような梃の原理は働きません。また、荷重の位置が中心線から左右にずれても、測定値が変化しないしくみです。それによって秤の皿を少し浮かしてとりつければかなり大きい面積が使えるというわけです。
用途に応じて、棒状でなくS型や丸型のロードセルも色々売られています。
さて、ありあわせの材料だけでテスト用のデジタル秤をチョコっと作ってみましたが、なんと楽なことか^^。秤量表示用のTM1637ディスプレイもセットしました。

まずは簡単なテストからやってみます。
①ゲージ側との接続

ロードセルのストレーンゲージから出ている赤,黒,緑,白の4つのワイヤーをHX711のE+,E-,A+,A-に接続。注意として、ストレーンゲージの配線は華奢(きゃしゃ)ですから、このときワイヤーの扱いに気を付けないといけませんね。私はロードセルから出ているワイヤーの根元を念のため事前にホットメルトで補強し、上の写真のようにワイヤークリップで台に固定してあまり動かないようにしました。
②マイコン側との接続
Arduino Unoからの5VとGndをHX711の電源に供給します。信号線はどのデジタルピンでを選んでもよいですが、近くのピンを使うことにして、UnoのA1(=D15として使う)からHX711SCKのSCKへ、そしてDT(またはdOut)からUnoのA0(=D14として使う)へとつなぎました。

そして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回ぶんの間隔)。

上は通常表示ですが、下は重さを対数軸にしたものです(よって負の点は表示されませんが)。これなら0.1g単位の表示はとても正確にできそうです。測定値は10回のサンプリング平均としていますが、これを増やせば0.01g単位での表示も可能だと思います。
なお、スケッチ中の
#define CalF 1885.F // individual LC caliblation factor
はロードセル毎のキャリブレーション値で、測定値をこれで割り算して重さ(g)に変換する除数(フロート型)です。重さの分かっている錘をあらかじめ計ることで値を決めます。
実に簡単とはいえ、とくに問題なく動いています。事前のネットでの簡単な調査ではノイズ問題の報告が色々みられました。一例は次のリンクです:
ひずみゲージADC HX711 のナゾシグナル | ココアシステムズ
ここで確認してみました。指摘時(2017年11月)はあった問題なのでしょうが、今では解決済みかと思われます。念のため在庫分の回転が速そうなセラーを選んたわけですが、シグナルをオシロで点検しておきました。
該当のノイズはどこにも出ていません。それをアナライザー(SCKをチャネル1、DataOUTをチャネル2)で記録したのが次です。

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

データシートによれば、以下が守られないといけません(守らないと誤読に至ります)。
①ADCが完了してから読取りを開始
Channel 1のクロック信号はArduino側から出しますが 、HX711がAD変換を終えてReadyになる(Data信号がLowになる)のを待ってから最初のHighを出して読取に入る。
②クロック信号のHighは60μS未満でなければならない。
観察結果は9.5μSですが時々最大15μSになります。しかし十分に収まっています。割り込み処理等が入る構造で1か所でもこれが守られないと誤読に至ります(60μS以上だとHX711がリセット信号とみなしリセット再開するためです)。
というわけでこれなら大丈夫。秤の簡単なテスト機を作ったのが次。本番機はもっとカッコよく、かなり小さく作れそうですね。

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 *****
次の動画ではこれを使って、
①錘を置く位置に関係なく重さが正しく測れること
②計測がリニアである(足し算が成り立つ)こと
をかんたんに説明したつもり^^; まずはクリックしてご覧いただくと幸い。
動画中で、分銅2つの合計の重さを計ったところがあります。

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

その後200g~400gを測るなどテストしましたがとても正確!
計測後のCreep現象(荷重で歪みが残る現象)も、数分の計測では全く生じませんでした。やっぱり大したものです^^
以上、ごく簡単ながら、正確でコスパのよい精密秤作成のご紹介でした。
以下蛇足。ご興味ある方はご覧ください。
===============================
「ダブルベンディングビーム型ロードセル」がなぜ双眼鏡のような形で切り抜かれているかについて、少し補足させていただきたいと思います。
中に単なる直方体を切り抜けば、歪がどうなるかが直観でも分かりやすいのですが、そうやると切り抜いた角から材料にヒビが入っていく問題(応力集中)が生じます。なので、内部に角ができないように丸みをつけているもの。
この形だと力をかけた時の内部の歪(または内部応力)の計算が難解のため、50年ぐらい前から有限要素法(Fininte element method:有限の格子の集まりとしたモデルを使う構造計算)が使われています。格子の設定1例と計算結果は次です。

伸びと縮みのツボが一目瞭然です。赤と青の表面がストレーンゲージを貼るべき位置となります。今回使ったのは上下のツボ合計4か所に貼られています。
=================================
以上、ごく簡単な工作とテストでしたが、どなたかのお役に立てば幸いです。
2021.10.13追記:
①HX711のSleep modeは必要か?
スケッチのLoopの最後のところに入れましたが、これついて補記させていただきます。
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時間後に温度を計測しました。

左右どちらのロードセルやゲージにも温度上昇はみられません。HX711モジュールは、Sleepさせていない右側だけが最大1.0℃上昇しました。ただし、測定値には何も影響はありませんでした(温度補償されているし、ゲージ側仕様でも通常の温度変化では影響がないものです)。ケースに収め長時間使うともう少し温度は上昇するとは思いますが。
②ノイズが入るものがある?
ネットで「ノイズで安定して計れない」という投稿や質問があるのですが、そういう場合は、念のために購入したゲージ貼付ロードセルを点検されることをお勧めします。私は10個求めたうちの2個で不良をみつけました。
点検と言っても、つなぐ前に赤黒緑白のどれか2つの間の抵抗をテスターで計ってみるだけです。テスターをつないだままでロードセルのワイヤー出口部分を軽く押すとノイズが出る(抵抗が変化する)ものがあります。
おそらく品質検査まではOKだったのでしょうが、とくに保護をせずに複数本をまとめて封筒で送って来るため、梱包時か送付中に引っ張られて不良を起こしたのではないかと推測します。取り出しのときもワイヤーをひっかけたりしないように注意しないといけませんね。せっかく良くできている部品なので。
この記事は次からご参照をいただいています:
©2021 Akira Tomninaga, All rights reserved.
音声認識モジュールLD3320(Arduino Unoで)

音声認識はスマホやアレクサ等々で普及しまくりましたが、マイコンでやろうとするとあまり簡単ではありません・・と思っていました。
今回試すのはvoice recognitionではなくspeech recognitionなのですが、日本語訳だとどちらも「音声認識」になるようです(・・?
「voice recognitionとspeech recognitionは明確に違う」と、例えば次のリンク先では英語で言っいるのですが、・・
https://www.totalvoicetech.com/difference-between-voice-recognition-and-speech-recognition/
ブラウザで翻訳すると「音声認識と音声認識は違います・・・」と訳されるのでワケワカメ^^;
バイオメトリクス的な応用か、言っていることを解釈するのか、という違いでしょうが、今回はスピーチ認識。
LD3320モジュールは、話し手を選ばずに語句の識別と照合をしてくれるという、都合のよい音声認識モジュールです。

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

ICには"IC route"という社名が書かれているので検索してみると、ホームページがあってこの製品のデータシートやユーザーマニュアル類だけでなく、フォーラム風の情報も満載。全て中国語ですが今やブラウザーが上手に訳してくれるので十分使えますね。
IC routeのホームページは次で、この企業は主に音声認識ICを作っている会社のようです。
LD3320モジュールはマイコンとSPIで接続しますが、信号は3.3Vでなければいけません。Unoの場合は、面倒でも5V~3.3Vの信号レベルコンバータを介します。8チャネルある次のコンバーターを使いました。CSをグランドに落とすだけにして節約しても信号5本は要るので、4チャネルでは無理そうですね。

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

テストでは、Arduino-IDEのシリアルモニターで表示をしますが、一応LED4色で単純な表示もしてみます。
解釈する音声は50種までで、プログラムの立ち上がりで装置へ与えます。中国語ベースのためPinYin(といってもアクセントや抑揚はない)で入れます。
日本語や他の言語でも近い表現を入れれば認識できるというわけです。この例では「ありがとう」をいれたらバッチリでした。発音をWebでチェックしながら恐る恐るいれてみましたが^^;
しかし、英語を入れるのはかなり難しいですね。英語はメロディ的でリズム感を要し、日本語のように子音母音だけでは決められない点、それに子音で中国語にはない発音も多いからでしょうかね。
Web検索しても英語の使用例にはたどり着かないのはそのためかも。
今回テストをしてみると、中国語はもちろん日本語も意外によく判定します。抑揚やアクセントの設定をとくにしないのに、よくもこんなことができるものです!
ほぼ何でも正しく解釈されるのに驚きますが、プログラムでいれてみた中国語の「黄色」の発音だけはうまく認識せず、結局スキップして別の表現を考え中。
今回作った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の中の説明には次のような記述があります。英語で使おうとするのはやめた方がいいかもしれません。

今回はマイコンで手軽にできる音声認識でした。作業で両手がふさがっているときの簡単な入力装置にすると便利そうです。
以上、何らかのお役にたてば幸いです。
©2021 Akira Tominaga, All rights reserved.