カワリモノ息子の技術メモ的な~

カワリモノ息子とその母の技術メモ的な〜

学校が苦手な息子くんの作品とその母の作品、はたまた日常などいろいろを在宅エンジニア母が綴る

M5StackでIKEAふう回転置き時計を作った

(今回は息子の母作品ブログです。)

私、ひらめきました。
M5StackでIKEAのクロック(回転していろいろ表示するやつ)が作れる!!作りたい!!

↓ 我が家のIKEAのクロック、クロッキス。壊れてしまって傾けても表示が変わらない…。
f:id:toriko0413:20200221215348j:plain

そして情報収集すべくネットを検索すると…なんともうすでに作っている方がいらっしゃいました( ゚Д゚)

qiita.com

しかもビンゴでM5Stack FIREで作っていらっしゃる!!
では私はこれをベースにさせていただき、さらに環境センサからBLEで受信した値を出力してみよう!と思いました。

M5Stackの傾きの取得

このQiita記事を参考に「MPU9250」を使用したんですがこれがどーーーしてもうまくいかない。

M5Stack FIREの加速度センサは何度かモデルチェンジされており、今回はMPU9250内蔵モデルを使用している

↑ Qiita記事より

調べてみたら私が2020年1月に買ったM5Stack FireではMPU9250ではなくMPU6886というものが搭載されていることが判明。

f:id:toriko0413:20200221200746j:plain
付属のちっちゃい説明書の赤丸部分。

それでMPU9250について検索したら

#define M5STACK_MPU6886 

ってやってから

  M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);
  M5.IMU.getAccelData(&accX,&accY,&accZ);
  M5.IMU.getAhrsData(&pitch,&roll,&yaw);
  M5.IMU.getTempData(&temp);

のように書くとどうやら各種値がとれるらしい!と判明したのでまずはこの値が確認できるように実装。

そして動かしてみて各値がどのように変化したかを確認。
どうやらgetAhrsDataで取得したpitchとrollで回転状態が判断できそうということがわかりこれを利用することにしました。

ところで今回使おうと思うgetAhrsData以外のものについても調べてみたところ
getGyroData …ジャイロの値を取得
getAccelData …加速度の値を取得
getAhrsData …3次元空間での回転状態を取得
ということでした。
私ハード初心者なもので「ジャイロ」って何。。って具合なもので、各値についてもっと勉強すべきなのですが、テキトーな性格なものでとりあえず早く動くものを作りたいということでまたの機会に(;^ω^)

typedef enum
{
  APP_STATE_0 = 0,        // 正位置:日付時刻
  APP_STATE_1 = 1,        // 左が下:湿度計 %
  APP_STATE_2 = 2,        // 右が下:温度計 ℃
  APP_STATE_3 = 3,        // 逆位置:気圧 hPa
} app_state_e;

app_state_e currentAngle = APP_STATE_0;           // 現在の方向

~中略~

app_state_e getAngle(){
  app_state_e angle = APP_STATE_0;

  M5.IMU.getAhrsData(&pitch,&roll,&yaw);
//  Serial.printf(" %5.2f   %5.2f   %5.2f   ", pitch, roll, yaw);
//  Serial.println("");

  if (pitch > -50.0F && pitch < 50.0F){
    if (roll < 0.0F){
      return APP_STATE_3;
    }else{
      return APP_STATE_0;
    }
  }else if (pitch >= 50.0F){
    return APP_STATE_2;
  }else if (pitch < -50.0F){
    return APP_STATE_1;
  }

  return angle;
}

getAngle関数で現在の回転方向を決定するように作りました。
これでなんとなく回転方向が取れそうです。

出来上がったのがこちら

わーはじめてTwitter埋め込んでみた^^

全体のソースコード

// define must ahead #include <M5Stack.h>
#define M5STACK_MPU6886 

// LCD
#define LCD_LARGE_BAR_WIDTH (10)
#define LCD_LARGE_BAR_LENGTH (30)
#define LCD_LARGE_BAR_CORNER_RADIUS (6)
#define LCD_LARGE_BAR_GAP (LCD_LARGE_BAR_WIDTH >> 1)

#define LCD_SMALL_BAR_WIDTH (4)
#define LCD_SMALL_BAR_LENGTH (12)
#define LCD_SMALL_BAR_CORNER_RADIUS (3)
#define LCD_SMALL_BAR_GAP (LCD_SMALL_BAR_WIDTH >> 1)

#define LCD_DIGITS_CLEAR_ELM_NO (8)

// Thermometer Screen
#define LCD_THERMOMETER_ICON_DISP_Y_POS (80)
#define LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS (80)

#include "BLEDevice.h"
#include <M5Stack.h>
#include <WiFi.h>

static String omronSensorAdress = "fd:e5:99:72:2c:29";
static BLEUUID serviceUUID("0c4c3000-7700-46f4-aa96-d5e974e32a54");
static BLEUUID    charUUID("0c4C3001-7700-46F4-AA96-D5E974E32A54");

float pitch = 0.0F;
float roll  = 0.0F;
float yaw   = 0.0F;

// OMRON環境センサの値
long seq;
long temp;
long humid;
long light;
long uv;
long pressValue;
long noise;
long accelX;
long accelY;
long accelZ;
long batt;
      
typedef enum
{
  APP_STATE_0 = 0,        // 正位置:日付時刻
  APP_STATE_1 = 1,        // 左が下:湿度計 %
  APP_STATE_2 = 2,        // 右が下:温度計 ℃
  APP_STATE_3 = 3,        // 逆位置:気圧 hPa
} app_state_e;

app_state_e currentAngle = APP_STATE_0;           // 現在の方向
app_state_e changingCurrentAngle;   // 方向変更中(変更中は値が定まらないのでテンポラリとして格納)
int32_t changingAngleCount;     // 方向変更後の連続数(連続して方向が定まったときに角度変更確定とする)

static int ANGLE_DECISION_TIME =  50;

boolean is_state_changed = true;
boolean is_drawing = false;
boolean is_initialize = true;     // 初回起動時

// NTP
//const char *ntp_server_1st = "ntp.nict.jp";
const char *ntp_server_1st = "time.windows.com";
const char *ntp_server_2nd = "time.google.com";
const long gmt_offset_sec = 9 * 3600; // 時差(秒換算)
const int daylight_offset_sec = 0;    // 夏時間

boolean is_blink = true;

const uint16_t digits_V_long[] =
    {
        0b0000001111111111, // 0
        0b0000001111000000, // 1
        0b0000011100111001, // 2
        0b0000011111100001, // 3
        0b0000011111000110, // 4
        0b0000010011100111, // 5
        0b0000010011111111, // 6
        0b0000001111000111, // 7
        0b0000011111111111, // 8
        0b0000011111100111, // 9
        0b0000000000000000, // off
};

void drawNumberVLong(uint8_t x_start, uint8_t y_start, uint8_t number, uint8_t bar_width, uint8_t bar_length, uint8_t bar_gap, uint8_t corner_radius, uint16_t color_value)
{
  if (number > 10)
  {
    number = 10;
  }
  // top
  if (digits_V_long[number] & 0b0000000000000001)
    M5.Lcd.fillRoundRect(x_start, y_start, bar_length, bar_width, corner_radius, color_value);
  // upper-left
  if (digits_V_long[number] & 0b0000000000000010)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000000000000100)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  // under-left
  if (digits_V_long[number] & 0b0000000000001000)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 2), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000000000010000)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 3), bar_width, bar_length, corner_radius, color_value);
  // bottom
  if (digits_V_long[number] & 0b0000000000100000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 4), bar_length, bar_width, corner_radius, color_value);
  // under-right
  if (digits_V_long[number] & 0b0000000001000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 3), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000000010000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 2), bar_width, bar_length, corner_radius, color_value);
  // upper-right
  if (digits_V_long[number] & 0b0000000100000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000001000000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  // center
  if (digits_V_long[number] & 0b0000010000000000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 2), bar_length, bar_width, corner_radius, color_value);
}

const uint8_t digits_normal[] =
    {
        0b00111111, // 0
        0b00110000, // 1
        0b01101101, // 2
        0b01111001, // 3
        0b01110010, // 4
        0b01011011, // 5
        0b01011111, // 6
        0b00110011, // 7
        0b01111111, // 8
        0b01111011, // 9
        0b00000000, // off
};

void drawNumberNormal(uint8_t x_start, uint8_t y_start, uint8_t number, uint8_t bar_width, uint8_t bar_length, uint8_t bar_gap, uint8_t corner_radius, uint16_t color_value)
{
  if (number > 10)
  {
    number = 10;
  }
  // top
  if (digits_normal[number] & 0b0000000000000001)
    M5.Lcd.fillRoundRect(x_start, y_start, bar_length, bar_width, corner_radius, color_value);
  // upper-left
  if (digits_normal[number] & 0b0000000000000010)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  // under-left
  if (digits_normal[number] & 0b0000000000000100)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  // bottom
  if (digits_normal[number] & 0b0000000000001000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 2), bar_length, bar_width, corner_radius, color_value);
  // under-right
  if (digits_normal[number] & 0b0000000000010000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  // upper-right
  if (digits_normal[number] & 0b0000000000100000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  // center
  if (digits_normal[number] & 0b0000000001000000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 1), bar_length, bar_width, corner_radius, color_value);
}

app_state_e getAngle(){
  app_state_e angle = APP_STATE_0;

  M5.IMU.getAhrsData(&pitch,&roll,&yaw);
//  Serial.printf(" %5.2f   %5.2f   %5.2f   ", pitch, roll, yaw);
//  Serial.println("");

  if (pitch > -50.0F && pitch < 50.0F){
    if (roll < 0.0F){
      return APP_STATE_3;
    }else{
      return APP_STATE_0;
    }
  }else if (pitch >= 50.0F){
    return APP_STATE_2;
  }else if (pitch < -50.0F){
    return APP_STATE_1;
  }

  return angle;
}

void monitorAngleTask(void* arg){
  while (1) {
     // 方向を検出
    app_state_e body_angle = getAngle();
    if (body_angle != currentAngle){
      if (changingCurrentAngle != body_angle){
        changingCurrentAngle = body_angle;
        changingAngleCount = 1;
      }else{
        changingAngleCount++;
      }
      // 変更後方向
      if (changingAngleCount >= ANGLE_DECISION_TIME){
        // 一定回数以上方向が同じ方向と認識したら変更後の方向に確定
        Serial.println("!!ANGLE CHANGE!!");
        switch (body_angle){
        case APP_STATE_0:
          Serial.println("UPSIDE");
          M5.Lcd.setRotation(1);
          break;
        case APP_STATE_1:
          Serial.println("LEFT");
          M5.Lcd.setRotation(2);
          break;
        case APP_STATE_2:
          Serial.println("RIGHT");
          M5.Lcd.setRotation(0);
          break;
        case APP_STATE_3:
          Serial.println("BOTTOM");
          M5.Lcd.setRotation(3);
          break;
        }
        currentAngle = body_angle;
        changingAngleCount = 0;
        is_state_changed = true;

        // 画面表示
        setDisplay();
        is_state_changed = false;
      }   
    }
    
    delay(1);
  }
}

long getParamFromAdvertisedData(std::string advertisedDataString, int pos, int length, String paramName) {
  // 38桁の16進表記文字列から対象のパラメータ位置で抜き出して10進数に変換して戻す
  std::string substrData = "";
  String manufData = "";
  Serial.print(paramName);
  Serial.print(": [hex] ");
  substrData = advertisedDataString.substr(pos, length); // 指定位置から指定桁数抜き出す
  Serial.print(substrData.c_str());

  if (length > 2){
    // 2桁以上(今回は4桁しかない)のときは反対にする 例)7d08→087d
    for(int i=length-2; i>=0; i=i-2){
      manufData.concat(substrData.substr(i, 2).c_str());  
    }
  }else{
    manufData = substrData.c_str();
  }

  // 16進数→10進数変換
  long ret = strtol(manufData.c_str(), NULL, 16);
  Serial.print(" [dec] ");
  Serial.println(ret);
  
  return ret;
}

/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.println("BLE Advertised Device found: ");
    std::string advertisedDataString = advertisedDevice.toString().c_str();
    Serial.println(advertisedDataString.c_str());  // このなかの文字列 manufacturer data:d502のあとがデータになる
  
    String deviceAddress = advertisedDevice.getAddress().toString().c_str();
    Serial.println(deviceAddress.c_str());
    //ペリフェラルをアドレスで判断
    if(deviceAddress.equalsIgnoreCase(omronSensorAdress)){

      int manufPos = advertisedDataString.find("manufacturer data: d502");
      Serial.print("manufPos: ");
      Serial.println(manufPos);

      seq = getParamFromAdvertisedData(advertisedDataString, manufPos+23, 2, "seq");
      
      temp = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2, 4, "temp");
      humid = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4, 4, "humid");
      light = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4, 4, "light");
      uv = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4, 4, "uv");
      pressValue = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4, 4, "press");
      noise = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4, 4, "noise");
      accelX = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4, 4, "accelX");
      accelY = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4, 4, "accelY");
      accelZ = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4, 4, "accelZ");
      batt = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4+4, 2, "batt");
      
    }// Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks

/**
 * 温度計を表示する
 */
void displayThermometerScreen(){
  uint16_t bkground_color = M5.Lcd.color565(0, 180, 0);
  static long temperature_prev = 0;
  if (is_state_changed == true){
    M5.Lcd.fillScreen(bkground_color);
//    is_state_changed = false;
  }

  // draw thermometer icon
  drawThermometerIcon(40, LCD_THERMOMETER_ICON_DISP_Y_POS, bkground_color);

  uint8_t tens_place, ones_place;

  // 温度は20.95度のとき2095のようになるのでまず小数点以上を取り出してから10の位と1の位に分ける
  long temperature = temp / 100;
  tens_place = static_cast<uint8_t>(temperature / 10);
  ones_place = static_cast<uint8_t>(temperature % 10);
  
  Serial.print("temperature = ");
  Serial.println(temperature);
  Serial.print("tens_place = ");
  Serial.println(tens_place);
  Serial.print("ones_place = ");
  Serial.println(ones_place);

  if (temperature != temperature_prev)
  {
    // デジタル表示値をクリア
    drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);

  }
  // デジタル値を表示
  drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, tens_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, ones_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // ℃の°
  M5.Lcd.drawEllipse(130 + LCD_LARGE_BAR_WIDTH + LCD_LARGE_BAR_LENGTH + LCD_LARGE_BAR_GAP, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, 3, 3, TFT_BLACK);
  // ℃のC
  M5.Lcd.drawChar(130 + LCD_LARGE_BAR_WIDTH + LCD_LARGE_BAR_LENGTH + LCD_LARGE_BAR_GAP + 10, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, 'C', TFT_BLACK, bkground_color, 4);

  temperature_prev = temperature;
}

/**
 * 湿度計を表示する
 */
void displayHumidityScreen(){
  uint16_t bkground_color = M5.Lcd.color565(0, 180, 0);
//  static uint8_t temperature_prev = 0;
  static long humid_prev = 0;
  if (is_state_changed == true)
  {
    M5.Lcd.fillScreen(bkground_color);
//    is_state_changed = false;
  }

  uint8_t tens_place, ones_place;

  // 湿度は39.97%のとき3997のようになるのでまず小数点以上を取り出してから10の位と1の位に分ける
  long humidity = humid / 100;
  tens_place = static_cast<uint8_t>(humidity / 10);
  ones_place = static_cast<uint8_t>(humidity % 10);
  
  Serial.print("humidity = ");
  Serial.println(humidity);
  Serial.print("tens_place = ");
  Serial.println(tens_place);
  Serial.print("ones_place = ");
  Serial.println(ones_place);

  if (humidity != humid_prev)
  {
    // デジタル表示値をクリア
    drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);

  }
  // デジタル値を表示
  drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, tens_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, ones_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // %
  M5.Lcd.drawChar(130 + LCD_LARGE_BAR_WIDTH + LCD_LARGE_BAR_LENGTH + LCD_LARGE_BAR_GAP + 10, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, '%', TFT_BLACK, bkground_color, 4);

  humid_prev = humidity;
}

/**
 * 温度計アイコンを表示する
 */
void drawThermometerIcon(uint32_t base_x_pos, uint32_t base_y_pos, uint16_t bk_color)
{
  M5.Lcd.drawEllipse(base_x_pos, base_y_pos, 7, 7, TFT_BLACK);
  M5.Lcd.drawRoundRect(base_x_pos - 3, base_y_pos - 35, 8, 35, 4, TFT_BLACK);
  M5.Lcd.fillEllipse(base_x_pos, base_y_pos, 6, 6, bk_color);
  M5.Lcd.fillEllipse(base_x_pos, base_y_pos, 4, 4, TFT_BLACK);
  M5.Lcd.fillRoundRect(base_x_pos - 1, base_y_pos - 35 + 8, 4, 25, 2, TFT_BLACK);
}


// System Clock
class SystemClock
{
private:
  struct tm time_info;
  time_t timer;

public:
  uint32_t year = 0;
  uint32_t month = 0;
  uint32_t day = 0;
  uint32_t hour = 0;
  uint32_t minute = 0;
  uint32_t week_day = 0;
  uint32_t second = 0;
  uint32_t prev_year = 0;
  uint32_t prev_month = 0;
  uint32_t prev_day = 0;
  uint32_t prev_hour = 0;
  uint32_t prev_minute = 0;
  uint32_t prev_week_day = 0;
  uint32_t prev_second = 0;
  void backupCurrentTime(void);
  void updateByNtp(void);
  void updateBySoftTimer(uint32_t elasped_second);
};

void SystemClock::backupCurrentTime(void)
{
  prev_year = year;
  prev_month = month;
  prev_day = day;
  prev_hour = hour;
  prev_minute = minute;
  prev_week_day = week_day;
  prev_second = second;
}

void SystemClock::updateBySoftTimer(uint32_t elasped_second)
{
  struct tm *local_time;
  time_t timer_add = timer + elasped_second;
  local_time = localtime(&timer_add);
  year = local_time->tm_year + 1900;
  month = local_time->tm_mon + 1;
  day = local_time->tm_mday;
  hour = local_time->tm_hour;
  minute = local_time->tm_min;
  week_day = local_time->tm_wday;
  second = local_time->tm_sec;
}

void SystemClock::updateByNtp(void)
{
  Serial.println("---NTP ACCESS---");
  if (!getLocalTime(&time_info))
  {
    year = 0;
    month = 0;
    day = 0;
    hour = 0;
    minute = 0;
    week_day = 0;
    second = 0;
    timer = 0;
  }
  else
  {
    year = time_info.tm_year + 1900;
    month = time_info.tm_mon + 1;
    day = time_info.tm_mday;
    hour = time_info.tm_hour;
    minute = time_info.tm_min;
    week_day = time_info.tm_wday;
    second = time_info.tm_sec;
    timer = mktime(&time_info);
  }
}

SystemClock cl_system_clock;
#define NTP_ACCESS_MS_INTERVAL (300000)

#define LCD_CLOCK_YMD_DISP_Y_POS (10)
#define LCD_CLOCK_MD_STR_DISP_Y_POS (45)
#define LCD_CLOCK_HM_DISP_Y_POS (100)
#define LCD_CLOCK_PM_STR_DISP_Y_POS (175)
#define LCD_CLOCK_ICON_DISP_Y_POS (200)
#define LCD_CLOCK_WEEK_STR_DISP_Y_POS (220)

/**
 * 時計を表示する
 */
void displayDateTimeScreen()
{
  static boolean ntp_access_flag = true;
  static uint32_t base_milli_time;
  uint32_t elasped_second = 0;
  uint32_t diff_milli_time = 0;

  uint16_t bkground_color = M5.Lcd.color565(200, 0, 0);
  if (is_state_changed == true)
  {
    M5.Lcd.fillScreen(bkground_color);
    ntp_access_flag = true;
//    is_state_changed = false;
  }

  if (ntp_access_flag == true)
  {
    base_milli_time = millis();
    Serial.print("base_milli_time:");
    Serial.println(base_milli_time);
    cl_system_clock.updateByNtp();
    ntp_access_flag = false;
  }
  else
  {
    diff_milli_time = millis() - base_milli_time;
    if (diff_milli_time > NTP_ACCESS_MS_INTERVAL)
    {
      ntp_access_flag = true;
    }
    // Serial.print("diff_milli_time:");
    // Serial.println(diff_milli_time);
    elasped_second = diff_milli_time / 1000;
    cl_system_clock.updateBySoftTimer(elasped_second);
  }

  M5.Lcd.setTextColor(TFT_BLACK);
  M5.Lcd.setTextSize(2);

  // Month
  if (cl_system_clock.month != cl_system_clock.prev_month)
  {
    drawNumberNormal(10, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(35, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(10, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.month / 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(35, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.month % 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);

  M5.Lcd.drawLine(60, LCD_SMALL_BAR_LENGTH * 2 + 10, 70, 10, TFT_BLACK);

  // Day
  if (cl_system_clock.day != cl_system_clock.prev_day)
  {
    drawNumberNormal(80, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(105, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(80, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.day / 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(105, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.day % 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);

  // Year
  if (cl_system_clock.year != cl_system_clock.prev_year)
  {
    drawNumberNormal(180, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(205, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(230, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(255, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(180, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.year / 1000), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(205, LCD_CLOCK_YMD_DISP_Y_POS, ((cl_system_clock.year % 1000) / 100), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(230, LCD_CLOCK_YMD_DISP_Y_POS, (((cl_system_clock.year % 1000) % 100) / 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(255, LCD_CLOCK_YMD_DISP_Y_POS, (((cl_system_clock.year % 1000) % 100) % 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);

  // hour
  if (cl_system_clock.hour != cl_system_clock.prev_hour)
  {
    drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    M5.Lcd.setTextColor(bkground_color);
    M5.Lcd.drawString("PM", 20, LCD_CLOCK_PM_STR_DISP_Y_POS);
    M5.Lcd.setTextColor(TFT_BLACK);
  }
  drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.hour / 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.hour % 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  if (is_blink == true)
  {
    M5.Lcd.fillEllipse(150, 120, 4, 4, TFT_BLACK);
    M5.Lcd.fillEllipse(150, 150, 4, 4, TFT_BLACK);
  }
  else
  {
    M5.Lcd.fillEllipse(150, 120, 4, 4, bkground_color);
    M5.Lcd.fillEllipse(150, 150, 4, 4, bkground_color);
  }

  // minute
  if (cl_system_clock.minute != cl_system_clock.prev_minute)
  {
    drawNumberNormal(185, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(250, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(185, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.minute / 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(250, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.minute % 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // MONTH DATE
  const String s_month = "MONTH";
  const String s_date = "DATE";
  M5.Lcd.drawString(s_month, 10, LCD_CLOCK_MD_STR_DISP_Y_POS);
  M5.Lcd.drawString(s_date, 80, LCD_CLOCK_MD_STR_DISP_Y_POS);

  uint8_t week_count = 0;
  const char aweek[7][4] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
  uint8_t day_week_now = 0;
  // day_week_now = subZeller(t_date_now.npt_year, t_date_now.ntp_month, t_date_now.ntp_day);
  day_week_now = cl_system_clock.week_day;
  for (week_count = 0; week_count < 7; ++week_count)
  {
    if ((week_count == day_week_now) && (is_blink == false))
    {
      M5.Lcd.setTextColor(bkground_color);
      M5.Lcd.drawString(aweek[week_count], week_count * 45 + 10, LCD_CLOCK_WEEK_STR_DISP_Y_POS);
      M5.Lcd.setTextColor(TFT_BLACK);
    }
    else
    {
      M5.Lcd.drawString(aweek[week_count], week_count * 45 + 10, LCD_CLOCK_WEEK_STR_DISP_Y_POS);
    }
  }
  is_blink = !is_blink;

  // clock icon
//  drawClockIcon(300, LCD_CLOCK_ICON_DISP_Y_POS);

  // t_date_prev = t_date_now;
  cl_system_clock.backupCurrentTime();
}

/**
 * 気圧計を表示する
 */
void displayPressScreen(){
  uint16_t bkground_color = M5.Lcd.color565(128, 128, 255);
  static long pressure_prev = 0;
  if (is_state_changed == true)
  {
    M5.Lcd.fillScreen(bkground_color);
//    is_state_changed = false;
  }

  uint8_t milions_place, hundreds_place, tens_place, ones_place;

  // 気圧は1032.6hPaのとき10326のようになるのでまず小数点以上を取り出してから位に分ける
  long pressure = pressValue / 10;
  milions_place = static_cast<uint8_t>(pressure / 1000);
  hundreds_place = static_cast<uint8_t>((pressure / 100) %10);
  tens_place = static_cast<uint8_t>((pressure / 10) % 100);
  ones_place = static_cast<uint8_t>(pressure % 10);
  
  Serial.print("pressure = ");
  Serial.println(pressure);
  Serial.print("milions_place = ");
  Serial.println(milions_place);
  Serial.print("hundreds_place = ");
  Serial.println(hundreds_place);
  Serial.print("tens_place = ");
  Serial.println(tens_place);
  Serial.print("ones_place = ");
  Serial.println(ones_place);

  if (pressure != pressure_prev)
  {
    // デジタル表示値をクリア
    drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(160, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(225, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
  }
  // デジタル値を表示
  drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, milions_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, hundreds_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(160, LCD_CLOCK_HM_DISP_Y_POS, tens_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(225, LCD_CLOCK_HM_DISP_Y_POS, ones_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // hPa
  M5.Lcd.drawChar(240, 65, 'h', TFT_BLACK, bkground_color, 4);
  M5.Lcd.drawChar(265, 65, 'P', TFT_BLACK, bkground_color, 4);
  M5.Lcd.drawChar(285, 65, 'a', TFT_BLACK, bkground_color, 4);

  pressure_prev = pressure;
}

/**
 * 現在の角度で判断して画面を作成する
 */
void setDisplay(){

  // 別タスクで画面描画中の時は待つ
  while(is_drawing){
    delay(1000);
  }
  is_drawing = true;

  switch (currentAngle){
    case APP_STATE_0:
      // 時計
      displayDateTimeScreen();
      break;
    case APP_STATE_1:
      // 湿度計
      displayHumidityScreen();
      break;
    case APP_STATE_2:
      // 温度計
      displayThermometerScreen();
      break;
    case APP_STATE_3:
      displayPressScreen();
      break;
  }
  is_drawing = false;
  
}

/**
 * 画面表示用スレッド
 */
void displayTask(void* arg){
  while (1) {
    if (is_initialize == true || is_state_changed == false){
      // 画面表示 (is_state_changed=trueのときはいったん無視)
      setDisplay();
      is_initialize = false;
    }
    delay(1000); // Delay a second between loops.
  }
}

/**
 * setup
 */
void setup() {
  
  M5.begin(true,false,true);
  M5.Power.begin();
  
  //以下時計の角度計測のため必要
  M5.IMU.Init();
  
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  WiFi.mode(WIFI_MODE_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED)
  {
    delay(100);
  }
  // init time setting
  configTime(gmt_offset_sec, daylight_offset_sec, ntp_server_1st, ntp_server_2nd);
  struct tm time_info;
  if (!getLocalTime(&time_info))
  {
    M5.Lcd.fillScreen(TFT_RED);
    delay(3000);
  }
  
  // 方向検知用スレッド
  xTaskCreatePinnedToCore(monitorAngleTask, "monitorAngleTask", 4096, NULL, 1, NULL, 0);

  // 画面表示用スレッド
  xTaskCreatePinnedToCore(displayTask, "displayTask", 4096, NULL, 1, NULL, 0);
  
} // End of setup.


/**
 * This is the Arduino main loop function.
 */
void loop() {
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5);

  delay(1000); // Delay a second between loops.
} // End of loop

WiFi.begin(ssid, password);
のSSIDとPASSWORDは環境に応じて値を設定してください。

f:id:toriko0413:20200221220503j:plain

大きな流れと工夫したところ

大きな流れとして、メインスレッドでBLE受信して、そのほかに方向検知用スレッドと画面表示用スレッドを動かすようにしました。

方向検知用スレッドで方向が変わったのを認識した後すぐに描画しており、そのあと画面表示用スレッドがかぶってしまうと描画がおかしなことになるので相互に制御を行いました。(is_drawingとis_state_changedのフラグ)

回転方向は回転途中に値が定まらないため、方向変更中の方向を保持して連続して方向が定まったときに角度変更確定とするようにしました。
ANGLE_DECISION_TIMEの値で何回連続して方向が定まったときに確定とするかの値を保持させるようにしました。もし画面がチラチラする場合はこの値を増やしたら良いかと思います。

参考にさせていただいた部分

画面にデジタル数字を表示させる関数
drawNumberVLong()
drawNumberNormal()
については、前述のこちらのQiita記事をまるっとそのまま使わせていただいています。ありがとうございます。

日時表示の画面を作る関数
displayDateTimeScreen()
温度の画面を作る関数
displayThermometerScreen()
も、ほぼこちらのQiita記事そのまま使わせていただいています。
ただし温度はBLEでオムロン環境センサから取得した値を表示しています。

オムロン環境センサからのセンサー値

温度の他、湿度・気圧の画面もオムロン環境センサからの値を表示させました。
↓ 過去記事参照
siroitori.hatenablog.com

日付の取得

日付の取得部分。Quiita記事では

const char *ntp_server_1st = "ntp.nict.jp";

とあったものをそのまま使ってたんですが、マイクロソフト信者の息子くんから下記に書き換えられました(;^ω^)

const char *ntp_server_1st = "time.windows.com";

どちらも正常に動作します。

余談ですが息子くんに教えてもらいましたけど、Windowsでもこれ設定するところあるんですね。そりゃそうかとは思いますが、全然知りませんでした。
f:id:toriko0413:20200221213655p:plain
↑ Windowsで時刻サーバを設定する画面

さいごに

今回これを作ってみて、とても勉強になりました。
MPU9250の使い方とか、日付・時刻を取得するところとか。

そして地味にこれ超便利です。時計として!時刻ずれないし。当たり前ですけど。
今後もデスク用の時計として普段使いしながら、さらに開発していきたいと思います。

実はわたし先週から首と腕の痛みがなおらないなーとおもっていたら今日MRI検査で頸椎の椎間板ヘルニアということが判明・・・。
健康のありがたみを感じますね。痛みと闘いながら頑張ります^^

そうそう、M5Stack FIREはマイクがあるのでこれで何かやりたいな~とかも考えているところです。



スター・はてブとても嬉しいです