Newer
Older
m5scp2_exp / FactoryTest / FactoryTest.ino
#include <M5Unified.h>
#include "fft.h"
// #include "esp_pm.h"
#include <esp_now.h>
#include <WiFi.h>

// #include <rom/crc.h>
// #include <driver/i2s.h>
// #include <driver/rmt.h>

#define ENABLE_I2S 1

// #define ENABLE_BLE 1
#ifdef ENABLE_BLE
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#endif

// #define ENABLE_OTA 1
#ifdef ENABLE_OTA
#include <ArduinoOTA.h>
#endif

#include "Battery.h"

typedef struct
{
    double x;
    double y;
    double z;
} point_3d_t;

typedef struct
{
    point_3d_t start_point;
    point_3d_t end_point;
} line_3d_t;

typedef struct
{
    double x;
    double y;
} point_2d_t;

double r_rand = PI / 180;

double r_alpha = 19.47 * PI / 180;
double r_gamma = 20.7 * PI / 180;

double sin_alpha = sin(19.47 * PI / 180);
double cos_alpha = cos(19.47 * PI / 180);
double sin_gamma = sin(20.7 * PI / 180);
double cos_gamma = cos(20.7 * PI / 180);

extern const unsigned char ImageData[768];
extern const unsigned char error_48[4608];
extern const unsigned char icon_ir[4608];
extern const unsigned char icon_wifi[4608];
#ifdef ENABLE_BLE
extern const unsigned char icon_ble[4608];
extern const unsigned char icon_ble_disconnect[4608];
#else
extern const unsigned char icon_ble[1];
extern const unsigned char icon_ble_disconnect[1];
#endif

bool TestMode = false;               // テストモード 常時ONにした (A/Bボタンを押しながら起動すれば、通常のFactoryTestでもテストモードになる)
bool startCoundDownShutdown = false; // 電源OFFのカウントダウンを開始するならtrue
bool startWebOTA = false;            // WebOTA(from Remote Signal)を開始するならtrue
#ifdef ENABLE_OTA
bool startOTA = false;
bool startOTAhandle = false;
#endif

void start_WebOTA();
bool wifi_setup();
void ntp_setup();
void wifi_down();

LGFX_Sprite Disbuff(&M5.Display); // 画面ちらつき防止スプライト(画面のバッファリング)

hw_timer_t *timer = NULL;
volatile SemaphoreHandle_t timerSemaphore;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
volatile uint8_t TimerCount = 0;

Battery battery = Battery();

void IRAM_ATTR onTimer() // タイマー処理(LEDをチカチカさせている)
{
    portENTER_CRITICAL_ISR(&timerMux);
    digitalWrite(19, TimerCount % 100); // LEDの明るさ
    TimerCount++;
    portEXIT_CRITICAL_ISR(&timerMux);
}

void checkAXPPress() // 電源ボタン押下チェック
{
    if (M5.BtnPWR.wasPressed()) // 電源ボタン押下したら
    {
        powerOffOrDeepSleep();
        // ESP.restart();
    }

    if (startCoundDownShutdown)
        countDownShutdown();

    if (startWebOTA)
        countDownWebOTA();
#ifdef ENABLE_OTA
    if (startOTA)
    {
        startOTA = false;
        OTA_Setup();
        startOTAhandle = true;
    }
    if (startOTAhandle)
        ArduinoOTA.handle();
#endif
}

void powerOffOrDeepSleep()
{
    // M5.Power.isCharging()
    if (true)
    {
        Disbuff.fillRect(0, 0, 240, 135, TFT_RED);
        Disbuff.setFont(&fonts::Font0);
        Disbuff.setCursor(12, 20);
        Disbuff.setTextColor(TFT_WHITE);
        Disbuff.setTextSize(3);
        Disbuff.printf("Enter Deep Sleep");
        Disbuff.pushSprite(0, 0);
        M5.delay(3000);
        esp_sleep_enable_timer_wakeup(7 * 86400 * 1000000ULL); // 1週間後に復帰
        esp_deep_sleep_start();
    }
    else
    {
        Disbuff.fillRect(0, 0, 240, 135, TFT_GREEN);
        Disbuff.setFont(&fonts::Font0);
        Disbuff.setCursor(12, 20);
        Disbuff.setTextColor(TFT_BLACK);
        Disbuff.printf("Power Off");
        Disbuff.pushSprite(0, 0);
        M5.delay(3000);
        M5.Power.powerOff(); // 電源OFF
    }
}

void Displaybuff() // Disbuffスプライトを表示する。(テストモードだったらTest Modeと表示する)
{
    Disbuff.setTextSize(1);
    Disbuff.setTextColor(TFT_GREENYELLOW);
    Disbuff.drawString("FactoryTest 2025", 10, 2, 1);
    Disbuff.setTextColor(TFT_WHITE);

    battery.batteryUpdate();

    // battery.showBattery();
    Disbuff.pushSprite(0, 0);
}
void mytone_switch(int freq, int duration)
{
    M5.Speaker.tone(freq, duration);
    // tone(GPIO_NUM_2, freq, duration);
    // M5.Display.setBrightness(255);
}

void ErrorDialog(uint8_t code, const char *str) // エラー表示(主にバッテリー切れ警告)
{
    Disbuff.fillRect(28, 20, 184, 95, Disbuff.color565(45, 45, 45));
    Disbuff.fillRect(30, 22, 180, 91, TFT_BLACK);
    // Disbuff.drawRect(30,22,180,91,Disbuff.color565(45,45,45));
    // Disbuff.setSwapBytes(true);
    Disbuff.pushImage(40, 43, 48, 48, (uint16_t *)error_48);

    Disbuff.setCursor(145, 37);
    Disbuff.setTextFont(2);
    Disbuff.printf("%02X", code);
    Disbuff.drawString("ERROR", 55 + 45, 10 + 27, 2);
    Disbuff.drawString("-----------------", 55 + 45, 30 + 27, 1);
    Disbuff.drawString(str, 55 + 45, 45 + 27, 1);
    Disbuff.drawString("check Hardware ", 55 + 45, 60 + 27, 1);
    Disbuff.pushSprite(0, 0);

    while ((!M5.BtnA.isPressed()) && (!M5.BtnB.isPressed()))
    {
        M5.update();
        checkAXPPress();
        M5.delay(100);
    }
    while ((M5.BtnA.isPressed()) || (M5.BtnB.isPressed()))
    {
        M5.update();
        checkAXPPress();
        mytone_switch(1000, 200);
    }
    Disbuff.setTextColor(TFT_WHITE);
    Disbuff.setTextFont(1);
}

bool point3Dto2D(point_3d_t *source, point_2d_t *point)
{
    point->x = (source->x * cos_gamma) - (source->y * sin_gamma);
    point->y = -(source->x * sin_gamma * sin_alpha) - (source->y * cos_gamma * sin_alpha) + (source->z * cos_alpha);
    return true;
}

bool point2DToDisPoint(point_2d_t *point, uint8_t *x, uint8_t *y)
{
    *x = point->x + 120;
    *y = 67 - point->y;
    return true;
}

bool printLine3D(LGFX_Sprite *display, line_3d_t *line, uint16_t color)
{
    uint8_t start_x, start_y, end_x, end_y;
    point_2d_t point;
    point3Dto2D(&line->start_point, &point);
    point2DToDisPoint(&point, &start_x, &start_y);
    point3Dto2D(&line->end_point, &point);
    point2DToDisPoint(&point, &end_x, &end_y);

    display->drawLine(start_x, start_y, end_x, end_y, color);

    return true;
}

void RotatePoint(point_3d_t *point, double x, double y, double z)
{
    if (x != 0)
    {
        point->y = point->y * cos(x * r_rand) - point->z * sin(x * r_rand);
        point->z = point->y * sin(x * r_rand) + point->z * cos(x * r_rand);
    }

    if (y != 0)
    {
        point->x = point->z * sin(y * r_rand) + point->x * cos(y * r_rand);
        point->z = point->z * cos(y * r_rand) - point->x * sin(y * r_rand);
    }

    if (z != 0)
    {
        point->x = point->x * cos(z * r_rand) - point->y * sin(z * r_rand);
        point->y = point->x * sin(z * r_rand) + point->y * cos(z * r_rand);
    }
}

void RotatePoint(point_3d_t *point, point_3d_t *point_new, double x, double y, double z)
{
    if (x != 0)
    {
        point_new->y = point->y * cos(x * r_rand) - point->z * sin(x * r_rand);
        point_new->z = point->y * sin(x * r_rand) + point->z * cos(x * r_rand);
    }

    if (y != 0)
    {
        point_new->x = point->z * sin(y * r_rand) + point->x * cos(y * r_rand);
        point_new->z = point->z * cos(y * r_rand) - point->x * sin(y * r_rand);
    }

    if (z != 0)
    {
        point_new->x = point->x * cos(z * r_rand) - point->y * sin(z * r_rand);
        point_new->y = point->x * sin(z * r_rand) + point->y * cos(z * r_rand);
    }
}

line_3d_t rect[12] = {
    {.start_point = {-1, -1, 1}, .end_point = {1, -1, 1}},
    {.start_point = {1, -1, 1}, .end_point = {1, 1, 1}},
    {.start_point = {1, 1, 1}, .end_point = {-1, 1, 1}},
    {.start_point = {-1, 1, 1}, .end_point = {-1, -1, 1}},
    {
        .start_point = {-1, -1, 1},
        .end_point = {-1, -1, -1},
    },
    {
        .start_point = {1, -1, 1},
        .end_point = {1, -1, -1},
    },
    {
        .start_point = {1, 1, 1},
        .end_point = {1, 1, -1},
    },
    {
        .start_point = {-1, 1, 1},
        .end_point = {-1, 1, -1},
    },
    {.start_point = {-1, -1, -1}, .end_point = {1, -1, -1}},
    {.start_point = {1, -1, -1}, .end_point = {1, 1, -1}},
    {.start_point = {1, 1, -1}, .end_point = {-1, 1, -1}},
    {.start_point = {-1, 1, -1}, .end_point = {-1, -1, -1}},
};

void MPU6886Test() // 加速度テスト
{
    float accX = 0;
    float accY = 0;
    float accZ = 0;

    double theta = 0, last_theta = 0;
    double phi = 0, last_phi = 0;
    double alpha = 0.2;

    line_3d_t x = {
        .start_point = {0, 0, 0},
        .end_point = {0, 0, 0}};
    line_3d_t y = {
        .start_point = {0, 0, 0},
        .end_point = {0, 0, 0}};
    line_3d_t z = {
        .start_point = {0, 0, 0},
        .end_point = {0, 0, 30}};

    line_3d_t rect_source[12];
    line_3d_t rect_dis;
    for (int n = 0; n < 12; n++)
    {
        rect_source[n].start_point.x = rect[n].start_point.x * 30;
        rect_source[n].start_point.y = rect[n].start_point.y * 30;
        rect_source[n].start_point.z = rect[n].start_point.z * 30;
        rect_source[n].end_point.x = rect[n].end_point.x * 30;
        rect_source[n].end_point.y = rect[n].end_point.y * 30;
        rect_source[n].end_point.z = rect[n].end_point.z * 30;
    }

    while ((!M5.BtnA.isPressed()) /*&& (!M5.BtnB.isPressed())*/)
    {
        auto imu_update = M5.Imu.update();
        if (imu_update)
        {
            m5::IMU_Class::imu_data_t d = M5.Imu.getImuData();
            // M5.Imu.getAccelData(&accX, &accY, &accZ);
            // M5.MPU6886.getAccelData(&accX, &accY, &accZ);
            accX = d.accel.x;
            accY = d.accel.y;
            accZ = d.accel.z;
        }
        if ((accX < 1) && (accX > -1))
        {
            theta = asin(-accX) * 57.295;
        }
        if (accZ != 0)
        {
            phi = atan(accY / accZ) * 57.295;
        }

        theta = alpha * theta + (1 - alpha) * last_theta;
        phi = alpha * phi + (1 - alpha) * last_phi;

        Disbuff.fillRect(0, 0, 240, 135, TFT_BLACK);
        Disbuff.setTextSize(1);
        Disbuff.setTextColor(TFT_WHITE);
        Disbuff.setCursor(10, 115);
        Disbuff.printf("%.2f", theta);
        Disbuff.setCursor(10, 125);
        Disbuff.printf("%.2f", phi);
        // Displaybuff();
        M5.delay(20);

        z.end_point.x = 0;
        z.end_point.y = 0;
        z.end_point.z = 60;
        RotatePoint(&z.end_point, theta, phi, 0);
        RotatePoint(&z.end_point, &x.end_point, -90, 0, 0);
        RotatePoint(&z.end_point, &y.end_point, 0, 90, 0);

        for (int n = 0; n < 12; n++)
        {
            RotatePoint(&rect_source[n].start_point, &rect_dis.start_point, theta, phi, (double)0);
            RotatePoint(&rect_source[n].end_point, &rect_dis.end_point, theta, phi, (double)0);
            printLine3D(&Disbuff, &rect_dis, TFT_WHITE);
        }
        // Disbuff.fillRect(0,0,160,80,BLACK);
        printLine3D(&Disbuff, &x, TFT_RED);
        printLine3D(&Disbuff, &y, TFT_GREEN);
        printLine3D(&Disbuff, &z, TFT_BLUE);
        /*
        Disbuff.setTextColor(TFT_WHITE);
        Disbuff.setTextSize(1);
        Disbuff.fillRect(0,0,52,18,Disbuff.color565(20,20,20));
        Disbuff.drawString("MPU6886",5,5,1);
        */
        Displaybuff();
        last_theta = theta;
        last_phi = phi;

        M5.update();
        checkAXPPress();
    }
    while ((M5.BtnA.isPressed()) || (M5.BtnB.isPressed()))
    {
        M5.update();
        checkAXPPress();
        mytone_switch(1000, 200);
    }
    Disbuff.setTextColor(TFT_WHITE);
}

SemaphoreHandle_t xSemaphore = NULL;
SemaphoreHandle_t start_dis = NULL;
SemaphoreHandle_t start_fft = NULL;
int8_t i2s_readraw_buff[2048];
uint8_t fft_dis_buff[241][128] = {0};
uint16_t posData = 160;
char fftmes[30];
int fftmax, fftmaxidx;

void MicRecordfft(void *arg) // フーリエ変換
{
#ifdef ENABLE_I2S
    // int16_t *buffptr;
    // size_t bytesread;
    // uint16_t count_n = 0;
    // float adc_data;
    // double data = 0;
    // uint16_t ydata;
    // uint16_t count_offset = 1;

    // while (1)
    // {
    //     xSemaphoreTake(start_fft, portMAX_DELAY);
    //     xSemaphoreGive(start_fft);
    //     fft_config_t *real_fft_plan = fft_init(1024, FFT_REAL, FFT_FORWARD, NULL, NULL);
    //     i2s_read(I2S_NUM_0, (char *)i2s_readraw_buff, 2048, &bytesread, (100 / portTICK_RATE_MS)); // portTICK_RATE_MS は 1
    //     buffptr = (int16_t *)i2s_readraw_buff;

    //     // fftmax, fftmaxidx = real_fft_plan->size;

    //     for (count_n = 0; count_n < real_fft_plan->size; count_n++) // eal_fft_plan->sizeは1024
    //     {
    //         adc_data = (float)map(buffptr[count_n], INT16_MIN, INT16_MAX, -2000, 2000); // long map(long x, long in_min, long in_max, long out_min, long out_max)
    //         real_fft_plan->input[count_n] = adc_data;
    //     }
    //     fft_execute(real_fft_plan);

    //     xSemaphoreTake(xSemaphore, 100 / portTICK_RATE_MS);
    //     for (count_n = 1; count_n < real_fft_plan->size / 4; count_n++) // 1024/4=>256
    //     {
    //         data = sqrt(real_fft_plan->output[2 * count_n] * real_fft_plan->output[2 * count_n] + real_fft_plan->output[2 * count_n + 1] * real_fft_plan->output[2 * count_n + 1]);
    //         if ((count_n - 1) < 254)
    //         {
    //             data = (data > 3000) ? 3000 : data;
    //             ydata = map(data, 0, 3000, 0, 255);
    //             if (128 - count_n > 0)
    //                 fft_dis_buff[posData][128 - count_n] = ydata;
    //             if (ydata > 40 && fftmax < ydata)
    //             {
    //                 fftmax = ydata;
    //                 fftmaxidx = count_n;
    //             }
    //         }
    //     }

    //     posData++;
    //     if (posData >= 241)
    //     {
    //         posData = 0;
    //     }
    //     xSemaphoreGive(xSemaphore);
    //     fft_destroy(real_fft_plan);
    // }
#endif
}

void Drawdisplay(void *arg) // フーリエ変換のときの画面表示
{
    uint16_t count_x = 0, count_y = 0;
    uint16_t colorPos;
    while (1)
    {
        xSemaphoreTake(start_dis, portMAX_DELAY);
        xSemaphoreGive(start_dis);
        xSemaphoreTake(xSemaphore, 500 / portTICK_RATE_MS);
        for (count_y = 0; count_y < 128; count_y++)
        {
            for (count_x = 0; count_x < 240; count_x++)
            {
                if ((count_x + (posData % 240)) > 240)
                {
                    colorPos = fft_dis_buff[count_x + (posData % 240) - 240][count_y];
                }
                else
                {
                    colorPos = fft_dis_buff[count_x + (posData % 240)][count_y];
                }

                Disbuff.drawPixel(count_x, count_y, Disbuff.color565(ImageData[colorPos * 3 + 0], ImageData[colorPos * 3 + 1], ImageData[colorPos * 3 + 2]));
                /*
                disbuff[ count_y * 160 + count_x ].r =  ImageData[ colorPos * 3 + 0 ];
                disbuff[ count_y * 160 + count_x ].g =  ImageData[ colorPos * 3 + 1 ];
                disbuff[ count_y * 160 + count_x ].b =  ImageData[ colorPos * 3 + 2 ];
                */
            }
        }
        xSemaphoreGive(xSemaphore);

        Disbuff.setTextColor(WHITE);
        Disbuff.setTextSize(1);
        Disbuff.fillRect(0, 0, 235, 20, Disbuff.color565(20, 20, 20));
        if (fftmaxidx > 200)
        {
            Disbuff.fillRect(0, 0, 235, 20, Disbuff.color565(fftmax, 20, 20));
        }
        sprintf(fftmes, "max %3d  idx %3d  %5.0f Hz", fftmax, fftmaxidx, fftmaxidx * 41.67);
        Disbuff.drawString(fftmes, 5, 5, 2);
        fftmax = 0;
        fftmaxidx = 0;

        Disbuff.pushSprite(0, 0);
    }
}

TaskHandle_t xhandle_display = NULL;
TaskHandle_t xhandle_fft = NULL;

static constexpr const size_t record_number = 256;
static constexpr const size_t record_length = 200;
static constexpr const size_t record_size = record_number * record_length;
static constexpr const size_t record_samplerate = 16000;
static int16_t prev_y[record_length];
static int16_t prev_h[record_length];
static size_t rec_record_idx = 2;
static size_t draw_record_idx = 0;
static int16_t *rec_data;

void DisplayMicro()
{
    Disbuff.fillRect(0, 0, 160, 80, Disbuff.color565(0, 0, 0)); // 黒(R=0,G=0,B=0)で塗りつぶす
    Disbuff.pushSprite(0, 0);
#ifdef ENABLE_I2S
    prepareMic();
#endif

    // xSemaphoreGive(start_dis);                               // 画面表示タスク スタート
    // xSemaphoreGive(start_fft);                               // フーリエ変換タスク スタート
    M5.Display.setFont(&fonts::lgfxJapanGothic_16);
    M5.Display.startWrite();
    M5.Display.clear();
    M5.Display.setTextColor(WHITE, TFT_BLACK);
    rec_data = (typeof(rec_data))heap_caps_malloc(record_size * sizeof(int16_t), MALLOC_CAP_8BIT);
    memset(rec_data, 0, record_size * sizeof(int16_t));
    M5.Speaker.setVolume(132); // 0-255だが、あまり大きいと音が割れる

    while ((!M5.BtnA.isPressed())) // ABボタン押していない間、くりかえす
    {
        M5.update();
        if (M5.Mic.isEnabled())
        {
            static constexpr int shift = 6;
            auto data = &rec_data[rec_record_idx * record_length];
            if (M5.Mic.record(data, record_length, record_samplerate))
            {
                data = &rec_data[draw_record_idx * record_length];

                int32_t w = M5.Display.width();
                if (w > record_length - 1)
                {
                    w = record_length - 1;
                }
                for (int32_t x = 0; x < w; ++x)
                {
                    M5.Display.writeFastVLine(x, prev_y[x], prev_h[x], TFT_BLACK); // 以前の線を消す
                    int32_t y1 = (data[x] >> shift);
                    int32_t y2 = (data[x + 1] >> shift);
                    if (y1 > y2)
                    {
                        int32_t tmp = y1;
                        y1 = y2;
                        y2 = tmp;
                    }
                    int32_t y = (M5.Display.height() >> 1) + y1;
                    int32_t h = (M5.Display.height() >> 1) + y2 + 1 - y;
                    prev_y[x] = y;
                    prev_h[x] = h;
                    M5.Display.writeFastVLine(x, y, h, TFT_WHITE);
                }
                M5.Display.setCursor(0, 0);
                M5.Display.print("●マイク録音モード:\nBボタンで最新の3秒間を再生");

                M5.Display.display();

                if (++draw_record_idx >= record_number)
                {
                    draw_record_idx = 0;
                }
                if (++rec_record_idx >= record_number)
                {
                    rec_record_idx = 0;
                }
            }
        }

        if (M5.BtnB.wasPressed())
        {
            if (M5.Speaker.isEnabled())
            {
                M5.Display.clear();
                while (M5.Mic.isRecording())
                {
                    M5.delay(1);
                }

                /// Since the microphone and speaker cannot be used at the same time, turn off the microphone here.
                M5.Mic.end();
                M5.delay(20);
                M5.Speaker.begin();

                //        M5.Display.setCursor(0,0);
                //        M5.Display.print("再生中");
                int start_pos = rec_record_idx * record_length;
                if (start_pos < record_size)
                {
                    M5.Speaker.playRaw(&rec_data[start_pos], record_size - start_pos, record_samplerate, false, 1, 0);
                }
                if (start_pos > 0)
                {
                    M5.Speaker.playRaw(rec_data, start_pos, record_samplerate, false, 1, 0);
                }
                Serial.println("Playing...");
                for (int sec = 3; sec > 0; sec--)
                {
                    M5.Display.setCursor(0, 0);
                    M5.Display.printf("再生モード:\n  再生中 %d", sec);
                    M5.delay(1000);
                } // ここを実行しているあいだに、playRawの内容が再生されている。

                do
                {
                    M5.delay(10);
                    M5.update();
                    //        Serial.println("waiting end of play");
                } while (M5.Speaker.isPlaying()); // まだ再生中だったら待つ

                // Reset I2S and reinitialize microphone
                prepareMic();

                M5.Display.clear();
                //        M5.Display.setCursor(0,0);
                //        M5.Display.print("録音モード");

                //      }
            }
        }
        checkAXPPress();
    }

    while ((M5.BtnA.isPressed()) || (M5.BtnB.isPressed())) // ABボタン押している間、くりかえす
    {
        M5.update();
        checkAXPPress();
        M5.Display.endWrite();
        M5.Speaker.setVolume(45);
        mytone_switch(1000, 200);
    }
}

#define PIN_CLK 0
#define PIN_DATA 34

#ifdef ENABLE_I2S
void prepareMic()
{
    // This function will reset the I2S peripheral to clear any existing issues with channel allocation.
    M5.Speaker.end();
    delay(10);      // Add a small delay to allow hardware reset
    M5.Mic.begin(); // Reinitialize the microphone
    auto cfg = M5.Mic.config();
    cfg.noise_filter_level = 128;
    M5.Mic.config(cfg);
    //  Serial.println("resetI2S_end");
}
#endif

void DisplayRTC() // リアルタイムクロック(内蔵時計)
{
    Disbuff.fillRect(0, 0, 240, 135, Disbuff.color565(0, 0, 0));
    // Displaybuff();
    m5::rtc_time_t time;
    M5.Rtc.getTime(&time);

    while ((!M5.BtnA.isPressed()) && (!M5.BtnB.isPressed()))
    {
        Disbuff.fillRect(0, 0, 240, 135, Disbuff.color565(0, 0, 0));
        M5.Rtc.getTime(&time);
        Disbuff.setTextSize(4);
        Disbuff.setTextColor(TFT_GREENYELLOW);
        Disbuff.setCursor(25, 50);
        Disbuff.printf("%02d:%02d:%02d", time.hours, time.minutes, time.seconds);
        Disbuff.fillRect(0, 0, 240, 37, Disbuff.color565(20, 20, 20));
        Disbuff.setTextSize(2);
        Disbuff.setTextColor(TFT_WHITE);
        Disbuff.drawString("BM8563 RTC Time", 26, 19, 1);
        Disbuff.setTextSize(2);
        Disbuff.setCursor(6, 90);
        Disbuff.setTextColor(TFT_YELLOW);
        Disbuff.println("Press B to sync RTC from NTP");
        Displaybuff();
        M5.update();
        checkAXPPress();
        M5.delay(100);
    }
    if (M5.BtnB.isPressed())
    {
        // RTC sync from NTP
        // Wifi → NTP → update RTC → WifiOff
        wifi_down();
        if (wifi_setup())
            ntp_setup();
        wifi_down();
        M5.delay(100);

        Init_ESPNOW(); // ESPNOWの初期化

        M5.update();
        DisplayRTC(); // あまりよろしくないが、やっぱり時刻同期を確認したいので
    }
    while ((M5.BtnA.isPressed()) || (M5.BtnB.isPressed()))
    {
        M5.update();
        checkAXPPress();
        mytone_switch(1000, 200);
    }
    Disbuff.setTextColor(TFT_WHITE);
}

bool checkAXP192() // 電源・バッテリー管理モジュール
{
    float VBat = M5.Power.getBatteryVoltage();

    while (VBat < 3.2)
    {
        VBat = M5.Power.getBatteryVoltage();
        ErrorDialog(0x22, "Bat Vol error");
    }

    return true;
}

uint8_t crc8(uint8_t data, uint8_t *buff, uint32_t length) // 巡回冗長検査
{
    uint8_t bit;        // bit mask
    uint8_t crc = 0xFF; // calculated checksum
    uint8_t byteCtr;    // byte counter
    for (byteCtr = 0; byteCtr < length; byteCtr++)
    {
        crc ^= (buff[byteCtr]);
        for (bit = 8; bit > 0; --bit)
        {
            if (crc & 0x80)
            {
                crc = (crc << 1) ^ data;
            }
            else
            {
                crc = (crc << 1);
            }
        }
    }
    return crc;
}

void DisplayQRCode()
{
    while ((!M5.BtnA.isPressed()) /*&& (!M5.BtnB.isPressed())*/)
    {
        // M5.Display.fillRect(0, 0, 240, 135, TFT_BLACK);
        // M5.Display.qrcode("https://istlab.info", 20, 20, 120);
        Disbuff.fillRect(0, 0, 240, 135, TFT_WHITE);
        Disbuff.setFont(&fonts::lgfxJapanGothic_16);
        Disbuff.setTextSize(1);
        Disbuff.setCursor(0, 5);
        Disbuff.setTextColor(TFT_BLUE);
        Disbuff.println("↑電源ボタン\n←Aボタン\n\n詳しい使い方\n  QRコード→\n\nBボタン(側面)↓");
        Disbuff.qrcode("https://scrapbox.io/iot-programming/usage", 115, 8, 120);

        // M5.Display.setTextScroll(true);
        Disbuff.pushSprite(0, 0);

        checkAXPPress();
        M5.update();
        M5.delay(10);
        // count++;
    }
    while ((M5.BtnA.isPressed()) /*|| (M5.BtnB.isPressed())*/)
    {
        M5.update();
        checkAXPPress();
        mytone_switch(1000, 200);
    }
    Disbuff.setTextColor(TFT_WHITE);
}

void DisplayTestMode() // テストモード:ピンの電圧とバッテリー電圧
{
    float tempdata, humdata;
    uint8_t count_u = 0, count_t = 0;

    // #ifdef ENABLE_I2S
    //     i2s_pin_config_t pin_config;
    //     pin_config.bck_io_num = I2S_PIN_NO_CHANGE;
    //     pin_config.ws_io_num = 33;
    //     pin_config.data_out_num = I2S_PIN_NO_CHANGE;
    //     pin_config.data_in_num = PIN_DATA;
    //     i2s_set_pin(I2S_NUM_0, &pin_config);

    //     i2s_driver_uninstall(I2S_NUM_0);
    // #endif

    gpio_reset_pin(GPIO_NUM_0);
    gpio_reset_pin(GPIO_NUM_26);

    pinMode(26, OUTPUT);
    pinMode(25, INPUT_PULLDOWN);
    pinMode(36, INPUT_PULLDOWN);
    pinMode(0, OUTPUT);

    digitalWrite(0, 0);
    digitalWrite(26, 0);

    while ((!M5.BtnA.isPressed()) && (!M5.BtnB.isPressed()))
    {
        Disbuff.fillRect(0, 0, 240, 135, TFT_BLACK);
        Disbuff.setFont(&fonts::Font0);
        // Disbuff.setSwapBytes(true);
        Disbuff.setTextColor(Disbuff.color565(180, 180, 180));
        Disbuff.setTextSize(3);

        Disbuff.setCursor(12, 7);
        if (M5.Power.getBatteryVoltage() > 3.2) // バッテリーの電圧(残量によって変化する)
        {
            Disbuff.setTextColor(TFT_GREEN);
        }
        else
        {
            Disbuff.setTextColor(TFT_RED);
        }
        Disbuff.printf("b%4.2f", M5.Power.getBatteryLevel()); // バッテリー電圧を表示する

        Disbuff.setCursor(12, 37);
        if (M5.Power.getBatteryLevel() > 40)
        {
            Disbuff.setTextColor(TFT_GREEN);
        }
        else
        {
            Disbuff.setTextColor(TFT_RED);
        }
        Disbuff.printf("v%4.2f", M5.Power.getBatteryLevel()); // 外部電源電圧(5V←の電圧)

        digitalWrite(0, 0);
        digitalWrite(26, 0);
        count_u = 0;
        count_t = 0;

        for (int i = 0; i < 10; i++)
        {
            digitalWrite(0, i % 2);
            M5.delay(10);
            // pin36_adc = analogRead(36);
            if ((digitalRead(36) == HIGH) && (i % 2 == 1))
            {
                count_u++;
            }
            if ((digitalRead(25) == HIGH) && (i % 2 == 1))
            {
                count_t++;
            }
        }

        Disbuff.setCursor(110, 7);
        if (count_u >= 5)
        {
            Disbuff.setTextColor(TFT_GREEN);
            Disbuff.printf(" %d G0", count_u);
        }
        else
        {
            Disbuff.setTextColor(TFT_RED);
            Disbuff.printf(" %d G0", count_u);
        }
        Disbuff.setTextColor(TFT_WHITE);

        Disbuff.setCursor(110, 37);
        if (count_t >= 5)
        {
            Disbuff.setTextColor(TFT_GREEN);
            Disbuff.printf(" %d G25", count_t);
        }
        else
        {
            Disbuff.setTextColor(TFT_RED);
            Disbuff.printf(" %d G25", count_t);
        }
        Disbuff.setTextColor(TFT_WHITE);

        digitalWrite(0, 0);
        digitalWrite(26, 0);
        count_u = 0;

        for (int i = 0; i < 10; i++)
        {
            digitalWrite(26, i % 2);
            M5.delay(10);
            // pin36_adc = analogRead(36);
            if ((digitalRead(36) == HIGH) && (i % 2 == 1))
            {
                count_u++;
            }
        }

        Disbuff.setCursor(110, 67);
        if (count_u >= 5)
        {
            Disbuff.setTextColor(TFT_GREEN);
            Disbuff.printf(" %d G26", count_u);
        }
        else
        {
            Disbuff.setTextColor(TFT_RED);
            Disbuff.printf(" %d G26", count_u);
        }
        Disbuff.setTextColor(TFT_WHITE);

        digitalWrite(0, 0);
        digitalWrite(26, 0);
        // Serial.printf("G36 Vol:%d\n",analogRead(36));

        // if (count >= 10)
        // {
        //     count = 0;
        //     getTempAndHum(&tempdata, &humdata); // ENV. II SENSORをつないだときに、温度と湿度がとれる
        // }

        // Disbuff.setTextColor(TFT_WHITE); // 白色で
        // Disbuff.setCursor(12, 67);
        // Disbuff.printf(" %.1f", tempdata); // 温度?
        // Disbuff.setCursor(12, 97);
        // Disbuff.printf(" %.1f", humdata); // 湿度?

        Disbuff.pushSprite(0, 0);

        checkAXPPress();
        M5.update();
        M5.delay(10);
        // count++;
    }
    while ((M5.BtnA.isPressed()) || (M5.BtnB.isPressed()))
    {
        M5.update();
        checkAXPPress();
        mytone_switch(1000, 200);
    }
    Disbuff.setTextColor(TFT_WHITE);
}

esp_now_peer_info_t peerInfo;
void Init_ESPNOW()
{ // ESPNowの初期化
    // 引用: https://101010.fun/iot/esp32-m5stickc-plus-esp-now.html
    WiFi.mode(WIFI_STA);
    WiFi.disconnect();
    if (esp_now_init() == ESP_OK)
    {
        Serial.println("ESP-Now Init Success");
    }
    else
    {
        Serial.println("ESP-Now Init failed");
        ESP.restart();
    }
    // マルチキャスト用Slave登録
    memset(&peerInfo, 0, sizeof(peerInfo));
    for (int i = 0; i < 6; ++i)
    {
        peerInfo.peer_addr[i] = (uint8_t)0xff;
    }
    esp_err_t addStatus = esp_now_add_peer(&peerInfo);
    if (addStatus == ESP_OK)
    {
        // Pair success
        Serial.println("Pair success");
        esp_now_register_send_cb(onESPNOWSent);    // 送信後のコールバック関数を指定する
        esp_now_register_recv_cb(onESPNOWReceive); /// 受信時のコールバック関数を指定する
    }
}
// 引用: https://101010.fun/iot/esp32-m5stickc-plus-esp-now.html
void onESPNOWReceive(const esp_now_recv_info_t *recv_info, const uint8_t *data, int data_len)
{
    // char macStr[18];
    // snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
    //          recv_info->src_addr[0], recv_info->src_addr[1], recv_info->src_addr[2],
    //          recv_info->src_addr[3], recv_info->src_addr[4], recv_info->src_addr[5]);
    if (strcmp((char *)data, "ESPNOW__SHUTDOWN") == 0) // 100,254に深い意味はない。uint8_tは0〜255の数値
    {
        startCoundDownShutdown = true; // シャットダウンタイマースタート予約(発動はcheckAXPPress()のなかで)
    }
    if (strcmp((char *)data, "ESPNOW__WEBOTA") == 0) // 99,254に深い意味はない。uint8_tは0〜255の数値
    {
        startWebOTA = true;
    }
}

void countDownShutdown()
{
    int countsec = 10;
    int subcount = 10;
    while ((!M5.BtnA.isPressed()) && (!M5.BtnB.isPressed()))
    {
        Disbuff.fillRect(0, 0, 240, 135, TFT_ORANGE);
        Disbuff.setFont(&fonts::Font0);
        Disbuff.setTextColor(TFT_BLACK);
        Disbuff.setTextSize(3);
        Disbuff.setCursor(12, 20);
        Disbuff.printf("%d sec to shutdown. press A to cancel.", countsec);

        Disbuff.pushSprite(0, 0);
        M5.update();
        M5.delay(100);
        subcount--; // subcountが10から0になると、約1秒
        if (subcount < 0)
        {
            countsec--; // カウントダウン秒数を減らす
            subcount = 10;
        }
        if (countsec < 0)
        {
            powerOffOrDeepSleep();
        }
    }
    startCoundDownShutdown = false;
    M5.delay(50);
    Disbuff.setTextSize(1);
    Disbuff.setTextColor(TFT_WHITE);
}
void countDownWebOTA()
{
    int countsec = 10;
    int subcount = 10;
    while ((!M5.BtnA.isPressed()) && (!M5.BtnB.isPressed()))
    {
        Disbuff.setFont(&fonts::Font0);
        Disbuff.fillRect(0, 0, 240, 135, TFT_PURPLE);
        Disbuff.setTextColor(TFT_WHITE);
        Disbuff.setTextSize(3);
        Disbuff.setCursor(12, 20);
        Disbuff.printf("%d sec to\n WebOTA.\n press A to cancel.", countsec);

        Disbuff.pushSprite(0, 0);
        M5.update();
        M5.delay(100);
        subcount--; // subcountが10から0になると、約1秒
        if (subcount < 0)
        {
            countsec--; // カウントダウン秒数を減らす
            subcount = 10;
        }
        if (countsec < 0)
        {
            start_WebOTA();
        }
    }
    startWebOTA = false;
    M5.delay(50);
    Disbuff.setTextColor(TFT_WHITE);
}

void onESPNOWSent(const uint8_t *mac_addr, esp_now_send_status_t status)
{
    char macStr[18];
    snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
             mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
    // Serial.print("Last Packet Sent to: ");
    // Serial.println(macStr);
    // Serial.print("Last Packet Send Status: ");
    // Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

void ESP_NOW_SendShutdown() // Bボタンを押したら、ESPNOWで周辺デバイスの電源OFF
{
    while ((!M5.BtnA.isPressed()) /*&& (!M5.BtnB.isPressed())*/)
    {
        Disbuff.setFont(&fonts::Font0);
        if (TestMode)
        {
            Disbuff.fillRect(0, 0, 240, 135, TFT_PURPLE);
            Disbuff.setTextColor(TFT_YELLOW);
            Disbuff.setTextSize(3);
            Disbuff.setCursor(12, 20);
            Disbuff.printf("Press the B button to WebOTA nearby devices");
        }
        else
        {
            Disbuff.fillRect(0, 0, 240, 135, TFT_BLUE);
            Disbuff.setTextColor(TFT_YELLOW);
            Disbuff.setTextSize(3);
            Disbuff.setCursor(12, 20);
            Disbuff.printf("Press the B button to turn off nearby devices");
        }

        Disbuff.pushSprite(0, 0);
        checkAXPPress();
        M5.update();
        if (M5.BtnB.wasReleasefor(3000))
        {
            Disbuff.fillRect(0, 0, 240, 135, TFT_ORANGE);
            Disbuff.setCursor(12, 20);
            Disbuff.setTextColor(TFT_BLACK);
            Disbuff.printf("WebOTA\n start!!");
            Disbuff.pushSprite(0, 0);
            for (int i = 4000; i > 1000; i -= 100)
            {
                tone(GPIO_NUM_2, i, 30);
                M5.delay(30);
            }
            M5.Display.setBrightness(255);
            M5.update();
            wifi_down();
            start_WebOTA(); // WebOTA.ino
        }
        if (M5.BtnB.wasReleasefor(100))
        {
            if (TestMode)
            {
                Disbuff.fillRect(0, 0, 240, 135, TFT_RED);
            }
            else
            {
                Disbuff.fillRect(0, 0, 240, 135, TFT_GREENYELLOW);
            }
            Disbuff.setCursor(12, 20);
            Disbuff.setTextColor(TFT_BLACK);
            Disbuff.printf("Send\n Shutdown\n Signal!!");
            Disbuff.pushSprite(0, 0);
            mytone_switch(1000, 200);
            M5.delay(200);
            mytone_switch(2000, 200);
            M5.delay(200);
            mytone_switch(4000, 200);
            M5.delay(200);
            // for (int i = 2000; i < 4000; i += 100)
            // {
            //     tone(GPIO_NUM_2, i, 30);
            //     M5.delay(30);
            // }
            // M5.Display.setBrightness(255);
            wifi_down();
            Init_ESPNOW(); // ESPNOWの初期化
            M5.delay(100);
            uint8_t data[50];
            if (TestMode)
            {
                sprintf((char *)data, "ESPNOW__WEBOTA"); // 送信する文字列
            }
            else
            {
                sprintf((char *)data, "ESPNOW__SHUTDOWN");
            }
            esp_err_t result = esp_now_send(peerInfo.peer_addr, data, sizeof(data));
            Serial.print("Send Status: ");
            if (result == ESP_OK)
            {
                Serial.println("Success");
            }
            else if (result == ESP_ERR_ESPNOW_NOT_INIT)
            {
                Serial.println("ESPNOW not Init.");
            }
            else if (result == ESP_ERR_ESPNOW_ARG)
            {
                Serial.println("Invalid Argument");
            }
            else if (result == ESP_ERR_ESPNOW_INTERNAL)
            {
                Serial.println("Internal Error");
            }
            else if (result == ESP_ERR_ESPNOW_NO_MEM)
            {
                Serial.println("ESP_ERR_ESPNOW_NO_MEM");
            }
            else if (result == ESP_ERR_ESPNOW_NOT_FOUND)
            {
                Serial.println("Peer not found.");
            }
            else
            {
                Serial.println("Not sure what happened");
            }
            M5.delay(500);
        }
        if (M5.BtnA.isPressed())
            break;
        M5.delay(100);
    }
    while ((M5.BtnA.isPressed()) /*|| (M5.BtnB.isPressed())*/)
    {
        M5.update();
        checkAXPPress();
        M5.Speaker.tone(3000, 200);
    }
    M5.delay(50);
    Disbuff.setTextColor(TFT_WHITE);
}

void ColorBar() // 起動直後のカラーバー表示
{
    float color_r, color_g, color_b;

    color_r = 0;
    color_g = 0;
    color_b = 255;

    for (int i = 0; i < 384; i = i + 4)
    {
        if (i < 128)
        {
            color_r = i * 2;
            color_g = 0;
            color_b = 255 - (i * 2);
        }
        else if ((i >= 128) && (i < 256))
        {
            color_r = 255 - ((i - 128) * 2);
            color_g = (i - 128) * 2;
            color_b = 0;
        }
        else if ((i >= 256) && (i < 384))
        {
            color_r = 0;
            color_g = 255 - ((i - 256) * 2);
            ;
            color_b = (i - 256) * 2;
            ;
        }
        Disbuff.fillRect(0, 0, 240, 135, Disbuff.color565(color_r, color_g, color_b));
        Displaybuff();
    }

    for (int i = 0; i < 4; i++)
    {
        switch (i)
        {
        case 0:
            color_r = 0;
            color_g = 0;
            color_b = 0;
            break;
        case 1:
            color_r = 255;
            color_g = 0;
            color_b = 0;
            break;
        case 2:
            color_r = 0;
            color_g = 255;
            color_b = 0;
            break;
        case 3:
            color_r = 0;
            color_g = 0;
            color_b = 255;
            break;
        }
        for (int n = 0; n < 240; n++)
        {
            color_r = (color_r < 255) ? color_r + 1.0625 : 255U;
            color_g = (color_g < 255) ? color_g + 1.0625 : 255U;
            color_b = (color_b < 255) ? color_b + 1.0625 : 255U;
            Disbuff.drawLine(n, i * 33.75, n, (i + 1) * 33.75, Disbuff.color565(color_r, color_g, color_b));
        }
    }
    Displaybuff();
    M5.delay(500);

    for (int i = 0; i < 4; i++)
    {
        switch (i)
        {
        case 0:
            color_r = 255;
            color_g = 255;
            color_b = 255;
            break;
        case 1:
            color_r = 255;
            color_g = 0;
            color_b = 0;
            break;
        case 2:
            color_r = 0;
            color_g = 255;
            color_b = 0;
            break;
        case 3:
            color_r = 0;
            color_g = 0;
            color_b = 255;
            break;
        }
        for (int n = 0; n < 240; n++)
        {
            color_r = (color_r > 2) ? color_r - 1.0625 : 0U;
            color_g = (color_g > 2) ? color_g - 1.0625 : 0U;
            color_b = (color_b > 2) ? color_b - 1.0625 : 0U;
            Disbuff.drawLine(239 - n, i * 33.75, 239 - n, (i + 1) * 33.75, Disbuff.color565(color_r, color_g, color_b));
        }
    }
    Displaybuff();
    M5.delay(500);
}

uint8_t addrcheckbuff[3] = {
    0x34, //
    0x51, //
    0x68  //
};

void setup()
{
    auto cfg = M5.config();
    cfg.serial_baudrate = 115200;
    M5.begin(cfg);

    M5.Display.setRotation(3); // 画面向きは横
    M5.Speaker.setVolume(45);
    M5.Speaker.tone(2000, 500);
    M5.Display.setBrightness(255);
    // M5.Display.setFont(&fonts::lgfxJapanGothic_16);
    // M5.Display.setTextScroll(true);
    // Disbuff.setSwapBytes(true);
    Disbuff.setColorDepth(16);
    Disbuff.createSprite(240, 135);
    Disbuff.fillRect(0, 0, 240, 135, Disbuff.color565(200, 50, 50));
    Disbuff.pushSprite(0, 0);
    M5.delay(500);

    M5.update();
    if (M5.BtnB.isPressed()) // Bボタンを押して起動したら、テストモード
    {
        M5.Speaker.tone(4000, 200);
        M5.delay(100);
        TestMode = true;

        while (M5.BtnB.isPressed())
        {
            M5.update();
            M5.delay(10);
        }
    }
    M5.Display.setBrightness(200);

    battery.setSprite(&Disbuff); // バッテリー残量表示
    battery.setPosAndSize(160, 1, 1);
    battery.setCheckRate(300);

    // // deleteBattery()時の塗りつぶし色を設定
    // battery.setDeleteBgColor(TFT_BLACK);
    // 電池図形と%表示の色を設定
    // battery.setTextColor(TFT_WHITE);

    Init_ESPNOW(); // ESPNOWの初期化

    if (!TestMode)
        ColorBar(); // ディスプレイ発色チェック

    checkAXP192(); // バッテリー電圧チェック。低下してたらエラーメッセージ表示

    // pinMode(19, OUTPUT); // LEDのポートを出力に設定
    // timerSemaphore = xSemaphoreCreateBinary(); // バイナリセマフォ作成
    // timer = timerBegin(0, 80, true);             // タイマーID=0, 80クロックで1カウントする, カウントアップならtrue
    // timerAttachInterrupt(timer, &onTimer, true); // 割り込み関数onTimer()を登録
    // timerAlarmWrite(timer, 30000, true);         // トリガー条件。50000カウントで発動。trueは繰り返し実行(falseにすると1回のみ)
    // timerAlarmEnable(timer);

    // xSemaphore = xSemaphoreCreateMutex(); // ミューテックス排他制御
    // start_dis = xSemaphoreCreateMutex();
    // start_fft = xSemaphoreCreateMutex();

    // xSemaphoreTake(start_dis, portMAX_DELAY); // フーリエ変換のときの画面表示タスクを「待ち」状態にする
    // xSemaphoreTake(start_fft, portMAX_DELAY); // FFTタスクを「待ち」状態にする

    // xTaskCreate(Drawdisplay, "Drawdisplay", 1024 * 2, (void *)0, 4, &xhandle_display);
    // xTaskCreate(MicRecordfft, "MicRecordfft", 1024 * 2, (void *)0, 5, &xhandle_fft);

    Disbuff.pushSprite(0, 0);
}
bool beepstate = false;

void loop()
{
    // それぞれのテスト中は、関数のなかのループがまわる
    // A(orB)ボタンを押したら、現在実行中の関数のループを抜け、次の関数を実行する
    MPU6886Test();   // 加速度・ジャイロ
    DisplayRTC();    // リアルタイムクロック
    DisplayMicro();  // マイク
    DisplayQRCode(); // QRコード
    // DisplayTestMode(); // バッテリー電圧
    ESP_NOW_SendShutdown(); // シャットダウン信号の送信

    M5.update();
    M5.delay(50);
}

#ifdef ENABLE_OTA
const char *ssid = "ics-ap";
const char *password = "jikkenics";
void OTA_Setup()
{
    WiFi.softAPdisconnect(true);
    M5.delay(1000);
    WiFi.mode(WIFI_STA);
    M5.delay(1000);

    WiFi.begin(ssid, password);
    int trycount = 0;
    while ((WiFi.status() != WL_CONNECTED))
    {
        M5.Speaker.tone(2000, 500);
        M5.delay(200);
        M5.Display.print(".");
        trycount++;
        if (trycount == 100)
        {
            M5.Power.powerOff();
        }
    }
    M5.Display.fillScreen(GREEN);
    M5.Display.setCursor(10, 50);
    M5.Display.setTextColor(BLACK, GREEN);
    M5.Display.println(" CONNECTED! ");
    M5.Speaker.tone(4000, 500);
    M5.delay(1000);

    // Port defaults to 3232
    ArduinoOTA.setPort(3232);
    // Hostname defaults to esp3232-[MAC]
    ArduinoOTA.setHostname("m5");
    // No authentication by default
    // ArduinoOTA.setPassword("");
    // Password can be set with it's md5 value as well
    // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
    // ArduinoOTA.setPasswordHash("2bf9b9f1272b09206f050251343dcfcc"); //
    ArduinoOTA
        .onStart([]()
                 {
        String type;
        if (ArduinoOTA.getCommand() == U_FLASH)
            type = "sketch";
        else // U_SPIFFS
            type = "filesystem"; })
        .onEnd([]()
               {
        M5.Speaker.tone(2000,500);
        M5.delay(150);
        M5.Speaker.tone(4000,500);
        M5.delay(150);
        M5.Speaker.tone(8000,500);
        M5.delay(300);
        M5.Speaker.tone(0,1); })
        .onProgress([](unsigned int progress, unsigned int total)
                    { ota_progress(progress, total); })
        .onError([](ota_error_t error) {});
    //    Serial.printf("Error[%u]: ", error);
    //    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    //    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    //    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    //    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    //    else if (error == OTA_END_ERROR) Serial.println("End Failed");

    ArduinoOTA.begin();
}

int prev_progress = -1;
char buf[30];
void ota_progress(unsigned int progress, unsigned int total)
{
    int cur_progress = (progress / (total / 100));
    sprintf(buf, "OTA %d%% done", cur_progress);
    if (prev_progress < cur_progress)
    {
        M5.Display.setCursor(0, 30, 1);
        M5.Display.fillScreen(BLACK);
        M5.Display.setTextColor(WHITE, BLACK);
        M5.Display.println(buf);
        prev_progress = cur_progress;
    }
}
#endif