こんにちは、ENGかぴです。
ESP32-WROOM-32EのWire(SPI)を使用すると加速度センサー(ADXL345)のデータを取得することができます。加速度センサーのイベントを発生させイベント発生前後の様子をSDカードに履歴として保存する方法をまとめました。
ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。また、ADXL345モジュール(秋月電子)を使用しています。
SDカードモジュールはmicroSDカードスロットレベルシフタ付きブレークアウト基板キット:AE-microSD-LLCNV(秋月電子)を使用しています。
下記記事で動作確認したADXL345のフォールイベントのデータをSDカードに保存します。
ESP32-WROOM-32Eで加速度の変化をグラフ表示する
WiFi通信を使って最新のデータを表示してSDカードに保存した履歴データと一致するか確認を行います。
ESP32-WROOM-32Eで動作確認したことについてリンクをまとめています。
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
SDカードに加速度のイベント履歴を保存
Arduino環境ではSDカードを操作する標準ライブラリが実装されています。SDカードモジュールの配線とライブラリの使用方法を説明します。
ESP32-WROOM-32EとSDカードモジュールの構成
ADXL345はWire(I2C)通信を使用します。Wireを使用する場合はプルアップ抵抗が必要ですがADXL345モジュールに実装されているため必要ありません。ADXL345のイベント発生をDIで検出するため17ピンをRISING(立ち上がり)として使用します。
SDカードモジュールの電源は5VなのでESP32-WROOM-32Eの5Vを供給しています。
SDカードの操作はSPI通信を使用します。ESP32-WROOM-32EはVSPIとHSPIがありますが、特に指定しない場合VSPIピンが有効になります。HSPIを使用する場合は下記記事に使用例をまとめていますので参考にしてください。
ESP32-WROOM-32EのSPIでBME280の情報を取得
ライブラリの準備と初期化
#include <SPI.h>
#include <SD.h>
File myfile; //SDカードの状態を格納
void setup() {
if(!SD.begin()){
//初期化失敗の処理
}
}
SDカードの操作はSPI通信を使用するためSD.hをインクルードする必要があります。SPI.hはSD.hないでインクルードされているためインクルードしなくても問題ありません。SPIのスレーブ選択のDOを指定する必要がありますがデフォルトで5ピンが指定されています。
SDカードのファイルに関する情報等を管理するにFile型のクラス変数を変数を宣言します。例ではmyfileの宣言しています。
SDライブラリのbegin()関数を使用してSDカードに関する情報を初期化します。引数にはSDカードを選択するためのスレーブセレクト(SS)のピン番号を指定します。指定しない場合はデフォルトの5ピンで初期化が行われます。
SDカードが挿入されていない場合など失敗した場合は戻り値がfalseになるので必要があれば失敗したときの処理を入れます。
PR:アクセンチュアの転職なら【コンサルアクシスコンサルティング】
データの書き込み(動作履歴の保存)
myfile = SD.open(filepath,FILE_WRITE);
if(myfile){
myfile.println(" ,x,y,z");
for(uint16_t i = 0; i< CHART_SZ; i++ ){
myfile.print(i);
myfile.print(",");
myfile.print(wave[0][i]);
myfile.print(",");
myfile.print(wave[1][i]);
myfile.print(",");
myfile.println(wave[2][i]);
}
myfile.close(); //ファイルを閉じる
}else{
Serial.println("SD-ERR");
}
open()関数でファイルを書き込みモードで開きます。open()関数の第1引数にはファイルのパスを指定し、第2引数に書き込みを示すFILE_WRITEを指定します。ファイルが開けたらprint()関数を使用してデータを書き込みます。
print()関数でデータの番号とX,Y,Z軸の加速度情報を書き込みます。データの間に”,”を入れているのはテキストデータをCSVファイルに変換したときにセルを分けるためです。書き込みデータの最後は改行コードを入れるためprintln()で書き込みを行います。
データを書き込んだ後はファイルを閉じるためclose()関数を使用します。
データの読み込み(参考)
本記事ではデータの読み込みは使用しませんが参考の為データの読み込みについてまとめています。
//ファイル名を指定して読み込む場合の例
String filepath = "sample.txt"; //SDカード内に保存するファイル名
if (SD.exists(filepath)) { //ファイルが存在するか
Serial.println("sample.txt exists.");
myfile = SD.open(filepath,FILE_READ); //ファイルを開く
if(myfile){ //ファイルが開けた場合
while(myfile.available()){
str ="";
str = myfile.readStringUntil('\n'); //読み込み
}
myfile.close(); //ファイルを閉じる
}
} else {
Serial.println("sample.txt doesn't exist.");
}
SDカードからデータを読み出す場合にファイルが存在しているかをexists()関数で確認します。引数には開くファイル名を含めたパスを指定します。
ファイルが存在するかを確認せずにopen()関数で読み込みを行っても読み込み失敗になるためexists()関数で確認してからファイルを開くかは好みになります。
ファイルが存在する場合はopen()関数でファイルを読み込み専用で開きます。第1引数にはファイルのパスを指定し、第2引数に読み込みを示すFILE_READを指定します。
ファイルのopenに成功するとFileオブジェクト(myfileで宣言した変数)に戻り値として状態が引き継がれるためmyfileを使ってデータの読み込みを行います。
Fileオブジェクトのavailable()関数を使って読み込むデータが存在するかを確認します。データが存在する場合は0より大きな値になるためRead()関数を使ってデータを読み込みます。例ではReadStringUntil()関数を使って改行コードが見つかるまでデータを読み込んでいます。
データを読み込んだ後はファイルを閉じるためclose()関数を使用します。
//ディレクトリのファイル名を表示
File root = SD.open("/");
File file = root.openNextFile();
while(file){
Serial.println(file.name());
file = root.openNextFile();
}
SDカード内のファイルを表示する例を示しています。open()関数に”/”を指定することで直下のディレクトリが開きます。openNextFile()でファイルを指定しながらname()関数でファイル名を取得しています。
while()で繰り返すことで存在するファイル名をすべて表示することができます。対象のファイルがない場合はfalseになるためwhile()から抜けます。
加速度データをグラフで表示する
ADXL345の加速度情報をWiFiでブラウザー上にグラフ表示する方法は下記記事にまとめています。本記事ではグラフ用のデータの準備のポイントのみを説明します。
ESP32-WROOM-32Eで加速度の変化をグラフ表示する
グラフ表示はChart.jsを使っています。Chart.jsを使ってブラウザー上にグラフ表示する方法を下記記事まとめています。
Chart.jsを使ってデータをブラウザー上でグラフ表示する
加速度データをグラフ表示するためのデータを準備します。グラフ表示する目安としてADXL345のイベントを使います。今回はFREE FALLがイベントを検出した前後250msの加速度情報を保存して表示します。
加速度情報を保管するバッファはリング構成にすることでxyzdata[]配列のサイズをオーバーしないようにデータを更新することができます。wpはxyzdata[]配列におけるデータの格納場所を示すポインタです。
FREE FALLイベントが発生すると250ms分の加速度情報を取得してからwave[]配列にイベント発生時の加速度情報を格納します。
adxl.getAcceleration(xyz); //XYZの加速度の取得
ax = xyz[0];
getdata.xyz[0][getdata.wp] = ax;
if(++getdata.wp >= CHART_SZ ){
getdata.wp = 0;
}
if( timEventWait > TIME_UP ){ //イベントが発生したらスタート
if( ++evescnt >= CHART_HALF_SZ ){ //イベント発生から250ms経過
int16_t rp;
uint16_t i;
rp = getdata.wp;
for( i = 0; i < CHART_SZ; i++){
wave[0][i] = getdata.xyz[0][rp];
if( ++rp >= CHART_SZ ){
rp = 0;
}
}
timEventWait = TIME_UP;
evescnt = 0;
}
}
getdata.xyz[]配列に格納している加速度情報を時系列でグラフで表示するために並び替えしながら格納します。例ではgetdata[]においてwpで現在の測定値を格納した後でwpを更新(wp+1)しているので更新後のwp(wp+1)の測定データが時系列でみた時getdata[]内の最古のデータになります。
最古のデータの位置をrpの初期値としてgetdata[]配列のサイズ分順に取得して格納することで時系列で整理したデータになります。
FREE FALLイベント発生直後からデータを取得するのではなくイベント発生以前のデータを含めて取得することでイベント発生の様子を確認することができます。
PR:スキマ時間を有効に!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
動作確認
WiFi通信を使用して加速度センサーの情報をグラフで表示します。同時にフォールイベントが発生したタイミングのシリアルプロッタの様子と比較します。SDカードに保存している履歴のデータについてもデータが一致することを確認します。
スマホ(Android)のGoogle ChromeでIPアドレス「192.168.11.2」を入力しリクエストを送るとESP32-WROOM-32Eからスマホに返信しFREE FALLが発生した時のデータの様子を示すグラフ表示されました。
シリアルプロッタでのグラフ表示を行ってFREE FALL発生時のデータと一致するかを確かめました。
シリアルプロッタのグラフとスマホのグラフを比較すると波形の形がほぼ一致していることが分かりました。位置がずれているのはシリアルプロッタをスクリーンショットするタイミングによるものです。SDカードに履歴が保存されているかを確認しました。
SDカード内に履歴が2つ保存されていました。グラフ表示しているのは最新のイベントなので「logData-2.text」のデータを確認して上記2つのデータと一致するかを確認しました。テキストデータをCSVデータに変換してエクセルデータで保存してグラフ表示しました。
エクセルで表示したグラフを確認するとスマホで確認したグラフ及びシリアルプロッタで確認したデータのグラフと一致していることが分かりました。SDカードに履歴が保存されていることが確認できました。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
#include <ADXL345.h>
#include <SPI.h>
#include <SD.h>
#define SPI_SS 5
#define SLAVE_ADRS 0x45
#define POLYNOMIAL 0x31
#define CHART_SZ 500
#define CHART_HALF_SZ (CHART_SZ/2)
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_ADXL_MAX 1
#define TIME_EVENT_WAIT_MAX 600
#define INT_DATA_READY_BIT_MASK 0x80
#define INT_SINGLE_TAP_BIT_MASK 0x40
#define INT_DOUBLE_TAP_BIT_MASK 0x20
#define INT_ACTIVITY_BIT_MASK 0x10
#define INT_INACTIVITY_BIT_MASK 0x08
#define INT_FREE_FALL_BIT_MASK 0x04
#define INT_WATERMARK_BIT_MASK 0x02
#define INT_OVERRUNY_BIT_MASK 0x01
#define ACT_TAP_ACT_X 0x40
#define ACT_TAP_ACT_Y 0x20
#define ACT_TAP_ACT_Z 0x10
#define ACT_TAP_TAP_X 0x04
#define ACT_TAP_TAP_Y 0x02
#define ACT_TAP_TAP_Z 0x01
#define PIN_DI1 17
//#define MONITER_USE
typedef struct{
uint16_t wp;
uint16_t rp;
double xyz[3][CHART_SZ];
}DATA_RING;
const char *ssid = "EngKapi1"; //SSID
const char *pass = "22223333"; //password
const IPAddress ip(192,168,11,2); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク
/* 変数宣言 */
WebServer Wserver(80);
uint32_t beforetimCnt = millis();
ADXL345 adxl;
byte intsrc; //割り込みイベント
byte intsrc2; //アクティブまたはタップイベントの内容
byte int2src;
int16_t timAdxl345get;
int16_t timEventWait = TIME_OFF;
volatile bool intboo1 = false;
double wave[3][CHART_SZ];
double xyzdata[3][CHART_SZ];
DATA_RING getdata;
uint16_t evescnt;
File myfile;
uint16_t logcnt = 1;
//String filepath ="logData-";
/* プロトタイプ宣言 */
void mainApp(void);
void HtmlSet(void);
void handleNotFound(void);
void charDataSet(void);
void InitAdxl345();
void Adxl345Rcv();
void IRAM_ATTR irt1(void);
void SdSave(void);
void setup() {
Serial.begin(115200);
if(!SD.begin()){
Serial.println("initialization failed!");
while (1);
}
//Wire.begin();
InitAdxl345();
WiFi.softAP(ssid, pass); //WiFiのアクセスポイントの設定
WiFi.softAPConfig(ip, ip, subnet); //アクセスポイントのIP及びサブネットマス
SPIFFS.begin();
Wserver.serveStatic("/myfavicon.ico",SPIFFS,"/myfavicon.ico");
Wserver.serveStatic("/chart.min.js",SPIFFS, "/chart.min.js");
Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
Wserver.begin(); //Webサーバーの開始
pinMode(PIN_DI1,INPUT_PULLDOWN);
attachInterrupt(PIN_DI1,irt1,RISING);
}
void loop() {
mainTimer();
mainApp();
Wserver.handleClient();
}
/* タイマ管理 */
void mainTimer(void){
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
if( timAdxl345get > TIME_UP ){
--timAdxl345get;
}
if( timEventWait > TIME_UP ){
--timEventWait;
}
}
}
/* DIがRISINGして割込み処理 */
void IRAM_ATTR irt1(void){
intboo1 = true;
}
/* メイン処理関数 */
void mainApp(void){
if( intboo1 ){ //INT1
intboo1 = false;
intsrc = adxl.getInterruptSource();
#ifdef MONITER_USE
if( intsrc & INT_SINGLE_TAP_BIT_MASK){
Serial.println("SINGLE_TAP");
}
if( intsrc & INT_DOUBLE_TAP_BIT_MASK){
Serial.println("DOUBLE_TAP");
}
if( intsrc & INT_ACTIVITY_BIT_MASK){
Serial.println("Activity");
}
if( intsrc & INT_INACTIVITY_BIT_MASK){
Serial.println("inactivity");
}
if( intsrc & INT_FREE_FALL_BIT_MASK){
Serial.println("FREE_FALL");
}
if( adxl.isActivitySourceOnX()){
Serial.println("ACT X");
}
if( adxl.isActivitySourceOnY()){
Serial.println("ACT Y");
}
if( adxl.isActivitySourceOnZ()){
Serial.println("ACT Z");
}
if(adxl.isTapSourceOnX()){
Serial.println("TAP X");
}
if(adxl.isTapSourceOnY()){
Serial.println("TAP Y");
}
if(adxl.isTapSourceOnZ()){
Serial.println("TAP Z");
}
#endif
if( intsrc & INT_FREE_FALL_BIT_MASK){
if( timEventWait == TIME_OFF ){
timEventWait = TIME_EVENT_WAIT_MAX;
}
}
}
if( timAdxl345get == TIME_UP ){
timAdxl345get = TIME_ADXL_MAX;
Adxl345Rcv();
}
if( timEventWait == TIME_UP ){
timEventWait = TIME_OFF;
}
}
/* 加速度情報の取得 */
void Adxl345Rcv(){
double xyz[3];
double ax,ay,az;
adxl.getAcceleration(xyz); //XYZの加速度の取得
ax = xyz[0];
ay = xyz[1];
az = xyz[2];
getdata.xyz[0][getdata.wp] = ax;
getdata.xyz[1][getdata.wp] = ay;
getdata.xyz[2][getdata.wp] = az;
if(++getdata.wp >= CHART_SZ ){
getdata.wp = 0;
}
if( timEventWait > TIME_UP ){
if( ++evescnt >= CHART_HALF_SZ ){ //イベント発生から250ms経過
int16_t rp;
uint16_t i;
rp = getdata.wp;
for( i = 0; i < CHART_SZ; i++){
wave[0][i] = getdata.xyz[0][rp];
wave[1][i] = getdata.xyz[1][rp];
wave[2][i] = getdata.xyz[2][rp];
if( ++rp >= CHART_SZ ){
rp = 0;
}
}
timEventWait = TIME_UP;
evescnt = 0;
SdSave();
}
}
#ifndef MONITER_USE
Serial.print("X:");
Serial.print(ax);
Serial.print(",");
Serial.print("Y:");
Serial.print(ay);
Serial.print(",");
Serial.print("Z:");
Serial.print(az);
Serial.println("");
#endif
}
/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
String str = "";
str += "<html lang=\"ja\">";
str += "<head>";
//str += "<meta http-equiv=\"refresh\" content=\"5\">";
str += "<meta charset=\"UTF-8\">";
str += "<title>Sensor SHT35</title>";
str += "<link rel='shortcut icon' href='/myfavicon.ico' />";
str += "<script src = '/chart.min.js'></script>";
str += "</head>";
str += "<body>";
str += "<h1>ESP32-ADXL345加速度センサ</h1>";
str += "<h2>WebServerライブラリを使用</h2>";
str += "<div style='height: 600px; width: 800px; margin: auto;'>";
str += "<canvas id='ChartID'></canvas>";
str += "</div>";
str += "<script>";
str += "var ctx = document.getElementById('ChartID').getContext('2d');";
str += "var myChart = new Chart(ctx, {";
str += "type: 'line',";
str += "data: {";
str += "labels: [ ";
for(uint16_t i = 0; i< CHART_SZ; i++ ){
str += i;
if(i != CHART_SZ-1 )str += ",";
}
str += "],";
str += "datasets: [{";
str += "label: 'X',";
str += "fill: false,";
str += "borderColor: 'blue',";
str += "borderWidth: 1,";
str += "pointRadius: 0,";
str += "pointHoverBorderWidth: 10,";
str += "data: [";
for(uint16_t i = 0; i< CHART_SZ; i++ ){
str += wave[0][i];
if(i != CHART_SZ-1 )str += ",";
}
str += "]";
str += "}, {";
str += "label: 'Y',";
str += "fill: false,";
str += "borderColor: 'red',";
str += "borderWidth: 1,";
str += "pointRadius: 0,";
str += "pointHoverBorderWidth: 10,";
str += "pointStyle: 'rect',";
str += "data: [";
for(uint16_t i = 0; i< CHART_SZ; i++ ){
str += wave[1][i];
if(i != CHART_SZ-1 )str += ",";
}
str += "]";
str += "}, {";
str += "label: 'Z',";
str += "fill: false,";
str += "borderColor: 'green',";
str += "borderWidth: 1,";
str += "pointRadius: 0,";
str += "pointHoverBorderWidth: 10,";
str += "pointStyle: 'triangle',";
str += "yAxisID: 'y',";
str += "data: [";
for(uint16_t i = 0; i< CHART_SZ; i++ ){
str += wave[2][i];
if(i != CHART_SZ-1 )str += ",";
}
str += "]";
str += "}]";
str += "},";
str += "options: {";
str += "responsive: true,";
str += "plugins: {";
str += "title: {";
str += "display: true,";
str += "text: 'ADXL345から取得した加速度',";
str += "font: {";
str += "size: 18,";
str += "},";
str += "},";
str += "},";
str += "scales: {";
str += "x: {";
str += "display: true,";
str += "stacked: false,";
str += "title: {";
str += "display: true,";
str += "text: 'サンプル数',";
str += "font: {";
str += "size: 16,";
str += "},";
str += "},";
str += "},";
str += "y: {";
str += "min: -4,";
str += "max: 4,";
str += "title: {";
str += "display: true,";
str += "text: '加速度[g]',";
str += "color: 'black',";
str += "font: {";
str += "size: 16,";
str += "},";
str += "},";
str += "ticks:{";
str += "display: true,";
str += "color: 'black',";
str += "},";
str += "},";
str += "},";
str += "},";
str += "})";
str += "</script>";
str += "</body>";
str += "</html>";
detachInterrupt(PIN_DI1);//htmlデータ送信時に割り込み禁止
Wserver.send(200,"text/html", str);
//HTTPレスポンス200でhtmlデータとして送信
attachInterrupt(PIN_DI1,irt1,RISING); //割り込み復帰
}
/* URLが存在しない場合の処理 */
void handleNotFound(void) {
String message = "File Not Found\n\n";
message += "URI: ";
message += Wserver.uri();
message += "\nMethod: ";
message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += Wserver.args();
message += "\n";
for (uint8_t i = 0; i < Wserver.args(); i++) {
message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
}
Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}
/* 加速度センサーの初期化 */
void InitAdxl345(){
byte dmy;
adxl.powerOn();
//set activity/ inactivity thresholds (0-255)
adxl.setActivityThreshold(75); //62.5mg per increment 75
adxl.setInactivityThreshold(75); //62.5mg per increment
adxl.setTimeInactivity(10); // how many seconds of no activity is inactive?
//look of activity movement on this axes - 1 == on; 0 == off
adxl.setActivityX(1);
adxl.setActivityY(1);
adxl.setActivityZ(1);
//look of inactivity movement on this axes - 1 == on; 0 == off
adxl.setInactivityX(1);
adxl.setInactivityY(1);
adxl.setInactivityZ(1);
//look of tap movement on this axes - 1 == on; 0 == off
adxl.setTapDetectionOnX(0);//0
adxl.setTapDetectionOnY(0);//0
adxl.setTapDetectionOnZ(1);//1
//set values for what is a tap, and what is a double tap (0-255)
adxl.setTapThreshold(50); //62.5mg per increment
adxl.setTapDuration(15); //625us per increment
adxl.setDoubleTapLatency(80); //1.25ms per increment
adxl.setDoubleTapWindow(200); //1.25ms per increment
//set values for what is considered freefall (0-255)
adxl.setFreeFallThreshold(5); //(5 - 9) recommended - 62.5mg per increment 7
adxl.setFreeFallDuration(20); //(20 - 70) recommended - 5ms per increment 45
adxl.setFullResBit(1);
adxl.setRangeSetting(4); //4g Range
adxl.setInterruptMapping( ADXL345_INT_SINGLE_TAP_BIT, ADXL345_INT1_PIN );
adxl.setInterruptMapping( ADXL345_INT_DOUBLE_TAP_BIT, ADXL345_INT1_PIN );
adxl.setInterruptMapping( ADXL345_INT_FREE_FALL_BIT, ADXL345_INT1_PIN );
adxl.setInterruptMapping( ADXL345_INT_ACTIVITY_BIT, ADXL345_INT1_PIN );
adxl.setInterruptMapping( ADXL345_INT_INACTIVITY_BIT, ADXL345_INT1_PIN );
adxl.setInterrupt( ADXL345_INT_SINGLE_TAP_BIT, 1);
adxl.setInterrupt( ADXL345_INT_DOUBLE_TAP_BIT, 1);
adxl.setInterrupt( ADXL345_INT_FREE_FALL_BIT, 1);
adxl.setInterrupt( ADXL345_INT_ACTIVITY_BIT, 1);
adxl.setInterrupt( ADXL345_INT_INACTIVITY_BIT, 1);
dmy = adxl.getInterruptSource(); //ダミーで割り込みビットをクリア
}
/* SDカードに履歴を保存*/
void SdSave(void){
String filepath ="/logData-";
filepath.concat(logcnt);
filepath.concat(".txt");
if (SD.exists(filepath)) {
++logcnt;
}
else{
myfile = SD.open(filepath,FILE_WRITE);
if(myfile){
myfile.println(" ,x,y,z");
for(uint16_t i = 0; i< CHART_SZ; i++ ){
myfile.print(i);
myfile.print(",");
myfile.print(wave[0][i]);
myfile.print(",");
myfile.print(wave[1][i]);
myfile.print(",");
myfile.println(wave[2][i]);
}
myfile.close(); //ファイルを閉じる
++logcnt;
}else{
Serial.println("SD-ERR");
}
}
}
本ソースコードでグラフ表示するためにChart.jsライブラリをESP32 Sketch Data Uploadを使ってフラッシュ領域に書き込んでおく必要があります。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR: わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジムPython入門講座の申込
最後まで、読んでいただきありがとうございました。
SDカードに履歴を保存できたことでブラウザ上でSDカードのファイルを選択してグラフ表示するなど応用例が広がりそうです。