griのアイコン画像
gri 2021年02月28日作成 (2021年03月20日更新)
製作品 製作品 閲覧数 1693
gri 2021年02月28日作成 (2021年03月20日更新) 製作品 製作品 閲覧数 1693

怠惰なリモートワーカーのための 家電自動制御つきポモドーロ時計

怠惰なリモートワーカーのための 家電自動制御つきポモドーロ時計

概要

リモートワークでだらけてしまうので、時間強制力が強めなポモドーロタイマーを作りました。
ついでに、毎日エアコンと照明をつけたり消したりするのが面倒なので、赤外線リモコンで自動化しました。
無駄に稼働しないように、自分が在宅中かどうかをBLEビーコン(今回はMAMORIO)で判別します。

機能

  • 始業アラーム
    朝9時に鳴って、止めると強制的にポモドーロタイマーがスタート

  • ポモドーロタイマー
    ポモドーロ終了のアラームを止めると強制的に休憩開始、逆もしかり

  • 照明制御
    在宅時の夕方に暗くなったらON、外出時と0時に強制OFF

  • エアコン制御
    在宅時の活動時間中に寒かったらON、外出時と0時に強制OFF

全体の構成

キャプションを入力できます

材料

主なもの

他に使ったもの

設置と動作の様子

通常の時計表示とセンサーユニット
前面

諸々の開発機も兼ねているので、拡張モジュールを常時つけっぱなし
側面

ポモドーロタイマーは集中を削がないようシンプルに表示
ポモドーロ中と休憩中

リモコンはロフトベッドの枠に設置
もうちょっときれいにつけよう

MAMORIOはキーホルダーに
aが小文字って今気づいた

プログラム

リモコン側(M5AtomLite)

リモコンの信号は、スケッチ例 > IRremoteESP8266 > IRrecvDumpV3​ を使って解析しました。
エアコン用は以下を変えるだけ。

  • MyDeviceIdを1に
  • エアコン用の赤外線信号に
  • M5AtomLite内蔵の赤外線を使うならピン番号は12に

照明用リモコンのプログラム

#include <M5Atom.h> #include "BLEDevice.h" #include <IRsend.h> #define MyManufacturerId 0xffff #define MyDeviceId 0 // 照明 #define kIrSendPin 32 // Grove #define kSendFrequencyKhz 38 #define tagNotFoundCnt 20 #define iBeaconId 0x0215 // iBeacon識別子 #define MyMamorioId 0xffffffff // 自分のMAMORIOの固有ID 4byte(Major+Minor) enum { LIGHT_OFF = 0, LIGHT_ON, LIGHT_MAX, }; IRsend irsend(kIrSendPin); BLEScan* pBLEScan; const uint16_t rawDataOn[83] = {3454, 1764, 408, 490, 380, 490, 378, 1334, 404, 1336, 404, 490, 380, 1336, 404, 466, 404, 466, 404, 466, 402, 1336, 404, 468, 402, 466, 402, 1336, 404, 466, 404, 1336, 404, 466, 404, 1338, 402, 466, 404, 466, 402, 1338, 402, 466, 404, 466, 402, 466, 402, 468, 402, 1362, 378, 466, 402, 1362, 378, 1362, 378, 490, 378, 1362, 380, 490, 378, 490, 378, 492, 378, 490, 380, 1362, 378, 492, 378, 492, 378, 1362, 378, 492, 378, 492, 378}; // UNKNOWN F6B92168 const uint16_t rawDataOff[83] = {3428, 1770, 404, 492, 378, 492, 378, 1362, 378, 1362, 378, 492, 376, 1364, 378, 492, 378, 492, 378, 492, 376, 1362, 378, 492, 378, 492, 376, 1364, 376, 494, 376, 1364, 376, 494, 376, 1366, 374, 494, 376, 518, 350, 1390, 350, 520, 350, 520, 350, 520, 348, 520, 350, 1392, 348, 1392, 348, 1392, 350, 1392, 348, 522, 348, 1394, 348, 522, 348, 548, 322, 548, 322, 1418, 322, 1420, 320, 548, 320, 576, 294, 1446, 294, 576, 292, 578, 292}; // UNKNOWN 1B9C2A92 template <typename T, size_t N> size_t arraySize(T (&arr)[N]) { return N; } bool isAtHome = false; uint8_t seq =0xFF; uint8_t command = LIGHT_OFF; uint32_t count = 0; void setup() { M5.begin(); BLEDevice::init(""); pBLEScan = BLEDevice::getScan(); pBLEScan->setActiveScan(false); irsend.begin(); M5.dis.drawpix(0, 0x000000); } void loop() { bool tagFound = false; bool cmdFound = false; BLEScanResults foundDevices = pBLEScan->start(3); int n = foundDevices.getCount(); for (int i=0; i<n; i++) { BLEAdvertisedDevice d = foundDevices.getDevice(i); if (d.haveManufacturerData()) { std::string data = d.getManufacturerData(); if ((data[1] << 8 | data[0]) == MyManufacturerId && data[2] != seq && data[3] == MyDeviceId) { cmdFound = true; seq = data[2]; command = data[4]; break; } else if ((data[2] << 8 | data[3]) == iBeaconId && (data[21] << 24 | data[22] << 16 |data[23] << 8 | data[24]) == MyMamorioId) { tagFound = true; } } } if (isAtHome != tagFound) { count = 0; isAtHome = tagFound; } else if (!tagFound) { if (count <= tagNotFoundCnt) { count++; if (count == tagNotFoundCnt) { irsend.sendRaw(rawDataOff, arraySize(rawDataOff), kSendFrequencyKhz); } } } if (cmdFound) { switch (command) { case LIGHT_OFF: irsend.sendRaw(rawDataOff, arraySize(rawDataOff), kSendFrequencyKhz); break; case LIGHT_ON: if (isAtHome) { irsend.sendRaw(rawDataOn, arraySize(rawDataOn), kSendFrequencyKhz); } break; dafault: break; } } }

本体(M5Stack)

スケッチ例 > M5Stack の中から、以下のサンプルをベースにしました。

  • 明るさ取得: Unit > LIGHT
  • 温度の取得: Unit > ENVII_SHT30_BMP280
  • 扇形の描画: Advanced > Display > TFT_ArcFill

BLEのアドバタイジングは、書籍「みんなのM5Stack入門」を参考に。

本体のプログラム

#include <M5Stack.h> #include <WiFi.h> #include <Wire.h> #include "Adafruit_Sensor.h" #include "BLEDevice.h" #include "SHT3X.h" #define LIGHT_THD 3000 #define WORK_MIN 25 #define REST_MIN 5 #define WORK_SEC (WORK_MIN * 60) #define REST_SEC (REST_MIN * 60) #define DEG2RAD 0.0174532925 #define JST (3600L * 9) enum { DEVICE_LIGHT = 0, DEVICE_AC, DEVICE_MAX, }; enum { POMO_STATE_NONE = 0, POMO_STATE_WORK, POMO_STATE_REST, POMO_STATE_MAX, }; enum { TURN_OFF = 0, TURN_ON, }; // ポモドーロタイマー typedef struct { uint8_t state; uint16_t secLeft; uint16_t degree; bool isWorkStart; uint8_t dailyTotal; } pomo_t; // 環境センサー typedef struct { float tmp; float hum; float pressure; uint16_t darkness; } env_t; volatile pomo_t pomo = {POMO_STATE_NONE, 0, 0, false, 0}; volatile env_t env = {0.0, 0.0, 0.0, 0}; const uint16_t kLightPin = 36; // PLUS Module Port.B const char *ssid = "ssid"; // 自宅のWi-FiのSSID const char *password = "password"; // 自宅のWi-Fiのパスワード const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"}; SHT3X sht30; BLEAdvertising *pAdvertising; uint8_t prevH = 24; uint8_t prevS = 60; struct tm tm; void setup() { M5.begin(); Serial.println("start"); // スピーカー M5.Speaker.begin(); M5.Speaker.mute(); // 画面表示 M5.Lcd.setBrightness(16); M5.Lcd.fillScreen(TFT_BLACK); // I2C Wire.begin(); // WiFi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); } // NTP configTime(JST, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); // BLE BLEDevice::init("M5Stack"); BLEServer *pServer = BLEDevice::createServer(); pAdvertising = pServer->getAdvertising(); } void loop() { M5.update(); if (pomo.isWorkStart) { // 始業アラーム中 if (M5.BtnA.wasPressed() || M5.BtnB.wasPressed() || M5.BtnC.wasPressed()) { // 何かボタン押すと止まってポモドーロ開始 pomo.isWorkStart = false; switchPomoState(); } else { // M5.update()で始業アラームが止まってしまうので、再度鳴らしている M5.Speaker.beep(); } } else { if (M5.BtnA.wasPressed()) { switchPomoState(); } if (M5.BtnB.wasPressed()) { switchAll(); } if (M5.BtnC.wasPressed()) { exitPomo(); } } if (getLocalTime(&tm)) { int s = tm.tm_sec; if (prevS != s) { // 1秒ごとに実行 prevS = s; switch (pomo.state) { case POMO_STATE_NONE: // 通常の時計表示 M5.Lcd.setCursor(0, 0); M5.Lcd.setTextSize(3); M5.Lcd.printf("%d/%2d/%2d (%s)\n\n\n", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, wd[tm.tm_wday]); M5.Lcd.setTextSize(8); M5.Lcd.printf("%02d:%02d\n", tm.tm_hour, tm.tm_min); M5.Lcd.setTextSize(3); M5.Lcd.printf("\n\n%2.1fC %2.0f%% %2d pomo\r\n", env.tmp, env.hum, pomo.dailyTotal); break; case POMO_STATE_WORK: case POMO_STATE_REST: if (pomo.secLeft) { // 残り時間表示 pomo.secLeft--; uint16_t sec_max = (pomo.state == POMO_STATE_WORK ? WORK_SEC : REST_SEC); while (pomo.degree <= (sec_max - pomo.secLeft) * 360 / sec_max) { fillArc(160, 120, pomo.degree++, 1, 90, 90, 90, TFT_BLACK); } } else { // 0になったらアラーム M5.Speaker.setVolume(2); M5.Speaker.beep(); } break; default: break; } } int h = tm.tm_hour; if (prevH != h) { // 1時間ごとに実行 prevH = h; // センサー読むときにノイズが鳴るので頻度を落としている env.darkness = analogRead(kLightPin); if (sht30.get() == 0) { env.tmp = sht30.cTemp; env.hum = sht30.humidity; } // 平日の朝9時に始業アラーム if (h == 9 && tm.tm_wday > 0 && tm.tm_wday < 6) { M5.Speaker.setVolume(2); M5.Speaker.beep(); pomo.isWorkStart = true; } if (h == 0) { pomo.dailyTotal = 0; } // 照明制御 if (h >= 16 && env.darkness > LIGHT_THD) { sendBLECommand(DEVICE_LIGHT, TURN_ON); } else if (h < 5 && env.darkness <= LIGHT_THD) { sendBLECommand(DEVICE_LIGHT, TURN_OFF); } // エアコン制御 if (h >= 6 && env.tmp < 20.0) { sendBLECommand(DEVICE_AC, TURN_ON); } else if (h < 5) { sendBLECommand(DEVICE_AC, TURN_OFF); } } } delay(50); } // ポモドーロ・休憩の終了アラームを停止して切り替え void switchPomoState() { Serial.print((String) "pomo.state: " + pomo.state + "-> "); M5.Speaker.mute(); M5.Lcd.fillScreen(TFT_BLACK); pomo.degree = 0; if (pomo.state == POMO_STATE_WORK) { pomo.state = POMO_STATE_REST; M5.Lcd.fillEllipse(160, 120, 100, 100, TFT_GREEN); pomo.secLeft = REST_SEC; } else { pomo.state = POMO_STATE_WORK; M5.Lcd.fillEllipse(160, 120, 100, 100, TFT_RED); pomo.secLeft = WORK_SEC; pomo.dailyTotal++; } Serial.println(pomo.state); } // 家電一括ON/OFF void switchAll() { static bool isOn = false; isOn = !isOn; for (int devId = 0; devId < DEVICE_MAX; devId++) { sendBLECommand(devId, isOn ? TURN_ON : TURN_OFF); } } // ポモドーロタイマーを抜けて時計表示へ void exitPomo() { M5.Speaker.mute(); M5.Lcd.fillScreen(TFT_BLACK); pomo.state = POMO_STATE_NONE; } // BLEアドバタイジング void sendBLECommand(uint8_t deviceId, uint8_t command) { static uint8_t bleSeq = 0; BLEAdvertisementData oAdvertisementData = BLEAdvertisementData(); oAdvertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | LE General Discoverable Mode // BLEにのせるデータを作る std::string strServiceData = ""; strServiceData += (char)0x06; // 長さ strServiceData += (char)0xff; // AD Type 0xFF; Manufacturer specific strServiceData += (char)0xff; // テスト用カンパニーID(下位バイト) strServiceData += (char)0xff; // テスト用カンパニーID(上位バイト) strServiceData += (char)bleSeq; // シーケンス番号 strServiceData += (char)deviceId; // 送信先デバイス strServiceData += (char)command; // 操作番号 oAdvertisementData.addData(strServiceData); pAdvertising->setAdvertisementData(oAdvertisementData); pAdvertising->start(); delay(1000); pAdvertising->stop(); delay(2000); bleSeq++; } // 扇型を描画 void fillArc(int x, int y, int start_angle, int seg_count, int rx, int ry, int w, unsigned int colour) { byte seg = 1; byte inc = 1; // Calculate first pair of coordinates for segment start float sx = cos((start_angle - 90) * DEG2RAD); float sy = sin((start_angle - 90) * DEG2RAD); uint16_t x0 = sx * (rx - w) + x; uint16_t y0 = sy * (ry - w) + y; uint16_t x1 = sx * rx + x; uint16_t y1 = sy * ry + y; // Draw colour blocks every inc degrees for (int i = start_angle; i < start_angle + seg * seg_count; i += inc) { // Calculate pair of coordinates for segment end float sx2 = cos((i + seg - 90) * DEG2RAD); float sy2 = sin((i + seg - 90) * DEG2RAD); int x2 = sx2 * (rx - w) + x; int y2 = sy2 * (ry - w) + y; int x3 = sx2 * rx + x; int y3 = sy2 * ry + y; M5.Lcd.fillTriangle(x0, y0, x1, y1, x2, y2, colour); M5.Lcd.fillTriangle(x1, y1, x2, y2, x3, y3, colour); // Copy segment end to sgement start for next segment x0 = x2; y0 = y2; x1 = x3; y1 = y3; } }

今後TODO

  • 1日の累積ポモドーロ数を表示したい 完了
  • 環境センサの値をグラフで見たい
  • BLE通信中に時計の更新が止まるのでマルチタスクにしたい

まとめ

己の惰性を律する一方で、助長している気もします。
QoLは上がりました。
M5Stackは楽しすぎて、無限増殖中です。

griのアイコン画像
独学組込みエンジニア。 オーディオ・電子楽器開発など。
ログインしてコメントを投稿する