オオクボ
Published

M5 Sleep movement monitor & turning reminder

This device tracks your body movements during sleep. If you're still for too long, it gently vibrates to encourage you to turn over.

BeginnerFull instructions provided2 hours279
M5 Sleep movement monitor & turning reminder

Things used in this project

Hardware components

M5Stack Core2 ESP32 IoT Development Kit
M5Stack Core2 ESP32 IoT Development Kit
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

M5 Sleep movement monitor & turning reminder Image

Code

M5 Sleep movement monitor & turning reminder Code

C/C++
#include <M5Core2.h>
#include <SD.h>
#include <cmath> // fabs を使うために必要

// --- モード定義 ---
enum Mode {
  MODE_SENTINEL,
  MODE_LOG_VIEWER
};
Mode currentMode = MODE_SENTINEL;

// --- 感度レベルの定義 ---
enum Sensitivity {
  SENS_HIGH,
  SENS_MEDIUM,
  SENS_LOW
};
Sensitivity currentSensitivity = SENS_MEDIUM; // 初期感度は「中」

// 状態管理
bool isSentinelActive = true;
unsigned long buttonDebounceDelay = 100;
unsigned long lastButtonPressTime = 0;

// 画面オフ機能
unsigned long screenOffTimeout = 30000;
unsigned long lastInteractionTime;
bool screenIsOff = false;

// 加速度センサー関連
float accX, accY, accZ;
float prevX, prevY, prevZ;
float threshold = 0.1; // 初期値はSENS_MEDIUMに対応

// 時間管理
unsigned long lastMoveTime;
unsigned long checkInterval = 100;
unsigned long noMoveLimit = 30 * 60 * 1000;
unsigned long lastCheck = 0;

bool isAlarmActive = false;
unsigned long alarmStartTime = 0;
const int alarmDuration = 2000; // アラームが2秒間続く

// 感度変更ポップアップ用変数
unsigned long sensitivityPopupTime = 0;
const int SENSITIVITY_POPUP_DURATION = 1500;

// SDカードログ関連
File logFile;
const char* logFilePath = "/sleep_log.csv";

// ログビューア用の変数
long logScrollPosition = 0;
const int LOG_LINES_PER_PAGE = 10;

// グラフ表示用
const int graphWidth = 320;
const int graphHeight = 150; 
const int graphX = 0;
const int graphY = 40;
int graphIndex = 0;
float xData[graphWidth];
float yData[graphWidth];
float zData[graphWidth];


// フォワード宣言
void drawLogPage();
void exitLogViewer();
void setSensitivity(Sensitivity level);
void drawSensitivityStatus();
void drawHeader();
void drawGraphGrid();
void drawUiFrame();

// ログ書き込み関数
void writeToLog(String event) {
  logFile = SD.open(logFilePath, FILE_APPEND);
  if (logFile) {
    logFile.print(millis());
    logFile.print(",");
    logFile.print(accX);
    logFile.print(",");
    logFile.print(accY);
    logFile.print(",");
    logFile.print(accZ);
    logFile.print(",");
    logFile.println(event);
    logFile.close();
  } else {
    if (currentMode == MODE_SENTINEL) M5.Lcd.println("Log Write Fail!");
  }
}

// グラフ描画関数
void drawGraph() {
  for (int i = 1; i < graphWidth; ++i) {
    int x1_plot = graphX + i - 1;
    int x2_plot = graphX + i;
    int y1_x = graphY + (graphHeight / 2) - (int)(xData[(graphIndex - graphWidth + i - 1 + graphWidth) % graphWidth] * (graphHeight / 2.5));
    int y2_x = graphY + (graphHeight / 2) - (int)(xData[(graphIndex - graphWidth + i + graphWidth) % graphWidth] * (graphHeight / 2.5));
    M5.Lcd.drawLine(x1_plot, constrain(y1_x, graphY, graphY + graphHeight -1), x2_plot, constrain(y2_x, graphY, graphY + graphHeight -1), RED);
    int y1_y = graphY + (graphHeight / 2) - (int)(yData[(graphIndex - graphWidth + i - 1 + graphWidth) % graphWidth] * (graphHeight / 2.5));
    int y2_y = graphY + (graphHeight / 2) - (int)(yData[(graphIndex - graphWidth + i + graphWidth) % graphWidth] * (graphHeight / 2.5));
    M5.Lcd.drawLine(x1_plot, constrain(y1_y, graphY, graphY + graphHeight-1), x2_plot, constrain(y2_y, graphY, graphY + graphHeight-1), GREEN);
    int y1_z = graphY + (graphHeight / 2) - (int)(zData[(graphIndex - graphWidth + i - 1 + graphWidth) % graphWidth] * (graphHeight / 2.5));
    int y2_z = graphY + (graphHeight / 2) - (int)(zData[(graphIndex - graphWidth + i + graphWidth) % graphWidth] * (graphHeight / 2.5));
    M5.Lcd.drawLine(x1_plot, constrain(y1_z, graphY, graphY + graphHeight-1), x2_plot, constrain(y2_z, graphY, graphY + graphHeight-1), BLUE);
  }
}

// デザイン関連の関数
void drawGraphGrid() {
  for (int x = graphX + 20; x < graphX + graphWidth; x += 20) {
    M5.Lcd.drawFastVLine(x, graphY, graphHeight, TFT_DARKGREY);
  }
  for (int y = graphY + 20; y < graphY + graphHeight; y += 20) {
    M5.Lcd.drawFastHLine(graphX, y, graphWidth, TFT_DARKGREY);
  }
  M5.Lcd.drawRect(graphX, graphY, graphWidth, graphHeight, TFT_DARKGREY);
}

void drawUiFrame() {
  M5.Lcd.fillScreen(BLACK);
  drawHeader();
  drawGraphGrid();
}


// --- ログビューア関連の関数 ---
void enterLogViewer() {
  currentMode = MODE_LOG_VIEWER;
  logScrollPosition = 0;
  drawLogPage();
}

void exitLogViewer() {
  currentMode = MODE_SENTINEL;
  drawUiFrame();

  M5.Lcd.fillRect(graphX, graphY, graphWidth, graphHeight, TFT_DARKGREY);
  M5.Lcd.setTextColor(WHITE, TFT_DARKGREY);
  M5.Lcd.setTextSize(3);
  M5.Lcd.drawCentreString("PAUSED", 160, 105, 4);
  
  M5.Lcd.fillRect(0, graphY + graphHeight, 320, 50, BLACK);
  M5.Lcd.fillRoundRect(10, 200, 90, 35, 5, TFT_YELLOW);
  M5.Lcd.setTextColor(BLACK, TFT_YELLOW);
  M5.Lcd.setTextSize(2);
  M5.Lcd.drawCentreString("PAUSED", 55, 208, 2);

  M5.Lcd.fillRoundRect(115, 200, 90, 35, 5, TFT_CYAN);
  M5.Lcd.setTextColor(BLACK, TFT_CYAN);
  M5.Lcd.drawCentreString("Log", 160, 208, 2);
}

void drawLogPage() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextColor(CYAN, BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.println("--- Log Viewer ---");
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);
  File file = SD.open(logFilePath);
  if (!file) {
    M5.Lcd.println("Log file not found.");
    return;
  }
  file.seek(logScrollPosition);
  for (int i = 0; i < LOG_LINES_PER_PAGE; i++) {
    if (!file.available()) {
      M5.Lcd.println("--- END OF LOG ---");
      break;
    }
    String line = file.readStringUntil('\n');
    M5.Lcd.println(line);
  }
  file.close();
  M5.Lcd.setCursor(0, 220);
  M5.Lcd.setTextColor(YELLOW, BLACK);
  M5.Lcd.print("BtnA:Up  BtnB:Exit  BtnC:Down");
}

void handleLogViewerLogic() {
  if (M5.BtnB.wasPressed()) {
    exitLogViewer();
    return;
  }
  if (M5.BtnC.wasPressed()) {
    File file = SD.open(logFilePath);
    if (file && file.seek(logScrollPosition)) {
      if (file.available()) {
        file.readStringUntil('\n');
        logScrollPosition = file.position();
        drawLogPage();
      }
    }
    file.close();
  }
  if (M5.BtnA.wasPressed()) {
    if (logScrollPosition == 0) return;
    File file = SD.open(logFilePath);
    if (file) {
      long prevLinePos = 0;
      long currentLinePos = 0;
      while(file.available()) {
        currentLinePos = file.position();
        file.readStringUntil('\n');
        if (file.position() >= logScrollPosition) {
          break;
        }
        prevLinePos = currentLinePos;
      }
      logScrollPosition = prevLinePos;
      drawLogPage();
    }
    file.close();
  }
}

// --- 感度設定と表示に関する関数 ---
void setSensitivity(Sensitivity level) {
  currentSensitivity = level;
  switch (currentSensitivity) {
    case SENS_HIGH:
      threshold = 0.05; // 高感度
      break;
    case SENS_MEDIUM:
      threshold = 0.1;  // 中感度
      break;
    case SENS_LOW:
      threshold = 0.2;  // 低感度
      break;
  }
}

void drawSensitivityStatus() {
  M5.Lcd.fillRect(260, 5, 55, 30, BLACK);
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(260, 10);
  String sensText = "S:";
  switch (currentSensitivity) {
    case SENS_HIGH:   sensText += "H"; break;
    case SENS_MEDIUM: sensText += "M"; break;
    case SENS_LOW:    sensText += "L"; break;
  }
  M5.Lcd.print(sensText);
}

void drawHeader() {
  M5.Lcd.fillRect(0, 0, 320, 35, BLACK);
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(10, 10);
  M5.Lcd.print("Sleep Sentinel");
  drawSensitivityStatus();
}

void setup() {
  M5.begin(true, true, true, false);
  M5.IMU.Init();
  M5.Axp.SetLDOVoltage(3, 3300);

  drawUiFrame();
  
  if (!SD.begin()) {
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(60, 100);
    M5.Lcd.println("SD Card Failed!");
    while (1);
  }
  
  logFile = SD.open(logFilePath, FILE_WRITE);
  if (logFile) {
    if (logFile.size() == 0) {
        logFile.println("Time(ms),AccX,AccY,AccZ,Event");
    }
    logFile.close();
  } else {
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(60, 100);
    M5.Lcd.println("Log File Failed!");
  }

  setSensitivity(SENS_MEDIUM);
  lastInteractionTime = millis();
  M5.IMU.getAccelData(&prevX, &prevY, &prevZ);
  lastMoveTime = millis();
}

void handleAlarm() {
  if (!isAlarmActive) return;

  // 設定した時間が経過したらアラームを停止
  if (millis() - alarmStartTime > alarmDuration) {
    isAlarmActive = false;
    M5.Axp.SetLDOEnable(3, false); // 振動を停止
  }
}

void stopAlarm() {
  if (isAlarmActive) {
    isAlarmActive = false;
    M5.Axp.SetLDOEnable(3, false);
  }
}

// 監視モードのメインロジック
void handleSentinelLogic() {
    M5.update();
    unsigned long currentMillis = millis();

    // 画面復帰処理
    if (M5.BtnA.wasPressed() || M5.BtnB.wasPressed() || M5.BtnC.wasPressed()) {
        if (screenIsOff) {
            M5.Lcd.wakeup();
            screenIsOff = false;
            drawUiFrame();
            if (!isSentinelActive) {
                M5.Lcd.fillRect(graphX, graphY, graphWidth, graphHeight, TFT_DARKGREY);
                M5.Lcd.setTextColor(WHITE, TFT_DARKGREY);
                M5.Lcd.setTextSize(3);
                M5.Lcd.drawCentreString("PAUSED", 160, 105, 4);
                M5.Lcd.fillRect(0, graphY + graphHeight, 320, 50, BLACK);
                M5.Lcd.fillRoundRect(10, 200, 90, 35, 5, TFT_YELLOW);
                M5.Lcd.setTextColor(BLACK, TFT_YELLOW);
                M5.Lcd.setTextSize(2);
                M5.Lcd.drawCentreString("PAUSED", 55, 208, 2);
                M5.Lcd.fillRoundRect(115, 200, 90, 35, 5, TFT_CYAN);
                M5.Lcd.setTextColor(BLACK, TFT_CYAN);
                M5.Lcd.drawCentreString("Log", 160, 208, 2);
            }
        }
        lastInteractionTime = currentMillis;
        stopAlarm();
    }
  
    // ボタンA: 開始/停止
    if (M5.BtnA.wasPressed() && (currentMillis - lastButtonPressTime > buttonDebounceDelay)) {
        lastButtonPressTime = currentMillis;
        isSentinelActive = !isSentinelActive;
        if (isSentinelActive) {
            lastMoveTime = currentMillis;
            M5.IMU.getAccelData(&prevX, &prevY, &prevZ);
            drawGraphGrid();
        } else {
            if (!screenIsOff) {
                M5.Lcd.fillRect(graphX, graphY, graphWidth, graphHeight, TFT_DARKGREY);
                M5.Lcd.setTextColor(WHITE, TFT_DARKGREY);
                M5.Lcd.setTextSize(3);
                M5.Lcd.drawCentreString("PAUSED", 160, 105, 4);
                M5.Lcd.fillRect(0, graphY + graphHeight, 320, 50, BLACK);
                M5.Lcd.fillRoundRect(10, 200, 90, 35, 5, TFT_YELLOW);
                M5.Lcd.setTextColor(BLACK, TFT_YELLOW);
                M5.Lcd.setTextSize(2);
                M5.Lcd.drawCentreString("PAUSED", 55, 208, 2);
                M5.Lcd.fillRoundRect(115, 200, 90, 35, 5, TFT_CYAN);
                M5.Lcd.setTextColor(BLACK, TFT_CYAN);
                M5.Lcd.drawCentreString("Log", 160, 208, 2);
            }
        }
    }
    
    // ボタンB: ログビューアへ
    if (!isSentinelActive && M5.BtnB.wasPressed()) {
        enterLogViewer();
        return;
    }
    
    // ボタンC: 感度変更
    if (isSentinelActive && M5.BtnC.wasPressed()) {
      int nextSens = (currentSensitivity + 1) % 3;
      Sensitivity newSens = (Sensitivity)nextSens;
      setSensitivity(newSens);
      drawSensitivityStatus();
      sensitivityPopupTime = millis();
    }

    // 画面オフ
    if (isSentinelActive && !screenIsOff && (currentMillis - lastInteractionTime > screenOffTimeout)) {
        M5.Lcd.sleep();
        screenIsOff = true;
    }
  
    handleAlarm();

    if (isSentinelActive) {
        if (currentMillis - lastCheck >= checkInterval) {
            lastCheck = currentMillis;
            M5.IMU.getAccelData(&accX, &accY, &accZ);

            if (!screenIsOff) {
                // データ更新と描画
                xData[graphIndex] = accX;
                yData[graphIndex] = accY;
                zData[graphIndex] = accZ;
                graphIndex = (graphIndex + 1) % graphWidth;
                M5.Lcd.fillRect(graphX, graphY, graphWidth, graphHeight, BLACK);
                drawGraphGrid();
                drawGraph();
                M5.Lcd.fillRect(0, graphY + graphHeight, 320, 240 - (graphY + graphHeight), BLACK);
                M5.Lcd.fillRoundRect(10, 200, 90, 35, 5, TFT_DARKGREEN);
                M5.Lcd.setTextColor(WHITE, TFT_DARKGREEN);
                M5.Lcd.setTextSize(2);
                M5.Lcd.drawCentreString("ACTIVE", 55, 208, 2);
                float deltaX = fabs(accX - prevX);
                float deltaY = fabs(accY - prevY);
                float deltaZ = fabs(accZ - prevZ);
                if (deltaX > threshold || deltaY > threshold || deltaZ > threshold) {
                    M5.Lcd.fillRoundRect(115, 200, 90, 35, 5, GREEN);
                    M5.Lcd.setTextColor(BLACK, GREEN);
                    M5.Lcd.drawCentreString("Moving", 160, 208, 2);
                } else {
                    M5.Lcd.fillRoundRect(115, 200, 90, 35, 5, TFT_DARKCYAN);
                    M5.Lcd.setTextColor(WHITE, TFT_DARKCYAN);
                    M5.Lcd.drawCentreString("Standby", 160, 208, 2);
                }
                unsigned long noMoveDuration = currentMillis - lastMoveTime;
                M5.Lcd.setTextColor(WHITE, BLACK);
                M5.Lcd.setTextSize(2);
                M5.Lcd.setCursor(220, 208);
                M5.Lcd.printf("%02lu:%02lu", (noMoveDuration / 1000) / 60, (noMoveDuration / 1000) % 60);

                // アラーム表示
                if (isAlarmActive) {
                    M5.Lcd.fillRect(0, 80, 320, 60, RED);
                    M5.Lcd.setTextColor(WHITE, RED);
                    M5.Lcd.setTextSize(3);
                    M5.Lcd.drawCentreString("ALARM!", 160, 105, 4);
                }

                // 感度変更ポップアップ
                if (sensitivityPopupTime > 0) {
                  if (currentMillis - sensitivityPopupTime > SENSITIVITY_POPUP_DURATION) {
                      sensitivityPopupTime = 0;
                  } else {
                      String sensText;
                      switch(currentSensitivity) {
                          case SENS_HIGH:   sensText = "Sensitivity: HIGH"; break;
                          case SENS_MEDIUM: sensText = "Sensitivity: MEDIUM"; break;
                          case SENS_LOW:    sensText = "Sensitivity: LOW"; break;
                      }
                      M5.Lcd.fillRect(50, 90, 220, 40, DARKGREY);
                      M5.Lcd.setTextColor(ORANGE, DARKGREY);
                      M5.Lcd.setTextSize(2);
                      M5.Lcd.drawCentreString(sensText, 160, 105, 2);
                  }
                }
            }

            // 動き検知とログ書き込み
            float deltaX_check = fabs(accX - prevX);
            float deltaY_check = fabs(accY - prevY);
            float deltaZ_check = fabs(accZ - prevZ);
            if (deltaX_check > threshold || deltaY_check > threshold || deltaZ_check > threshold) {
                lastMoveTime = currentMillis;
                writeToLog("Movement");
                if(isAlarmActive) {
                    stopAlarm();
                    drawUiFrame();
                }
            }
            prevX = accX;
            prevY = accY;
            prevZ = accZ;

            if (!isAlarmActive && (currentMillis - lastMoveTime > noMoveLimit)) {
                isAlarmActive = true;
                alarmStartTime = currentMillis;
                M5.Axp.SetLDOEnable(3, true); // 振動を開始
                writeToLog("NoMovementAlarm");
                lastMoveTime = currentMillis;
            }
        }
    } 
}

void loop() {
  switch (currentMode) {
    case MODE_SENTINEL:
      handleSentinelLogic();
      break;
    case MODE_LOG_VIEWER:
      M5.update();
      handleLogViewerLogic();
      break;
  }
}

Credits

オオクボ
1 project • 2 followers

Comments