Hardware components | ||||||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
| |||||
Staying in one position for too long during sleep can be unhealthy, as turning over is essential for good blood circulation and restful sleep. To address this, we developed a system that monitors your body movements throughout the night. When it detects a prolonged period of stillness, it provides a gentle prompt to encourage you to change position, thereby promoting a healthier sleep cycle.
The built-in accelerometer detects and records body movements in real-time. This feature allows you to track when you change positions throughout the night.
If the monitor detects no movement for a prolonged period, a reminder will activate. This gentle prompt encourages you to turn over, preventing you from remaining in one position for too long.
3.System Configuration & ControlsHardware: The core of this device is the M5Stack Core2 for AWS, which utilizes its built-in 6-axis IMU (Inertial Measurement Unit) to detect movement.
Controls:- Left Button: Starts and stops the monitoring process.
- Center Button: Opens the log viewer when monitoring is paused.
Right Button: Adjusts the sensitivity of the 6-axis IMU.
Prerequisites
- Hardware: M5Stack Core2 for AWS
- Software: Arduino IDE
Installation Steps
- Prepare Your Environment: First, gather the required hardware and set up the Arduino IDE on your computer. You will also need to configure your computer for M5Stack development by installing the necessary board managers and drivers.
- Upload the Code: Using the Arduino IDE, upload the code provided in the 'Code' section of this page to your M5Stack Core2 for AWS.
The system is currently configured to activate a reminder when the duration of detected stillness exceeds a predefined threshold.
For future development, we plan to implement a feature that allows users to customize this activation time. Additionally, we will explore other enhancements to further improve the overall user experience and functionality.
#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;
}
}




_1SvYexGSwK.jpg)



Comments