編集履歴一覧に戻る
griのアイコン画像

gri が 2021年03月20日15時18分47秒 に編集

コメント無し

記事種類の変更

+

製作品

本文の変更

# 概要 リモートワークでだらけてしまうので、時間強制力が強めなポモドーロタイマーを作りました。 ついでに、毎日エアコンと照明をつけたり消したりするのが面倒なので、赤外線リモコンで自動化しました。 無駄に稼働しないように、自分が在宅中かどうかをBLEビーコン(今回はMAMORIO)で判別します。 # 機能 - 始業アラーム 朝9時に鳴って、止めると強制的にポモドーロタイマーがスタート - ポモドーロタイマー ポモドーロ終了のアラームを止めると強制的に休憩開始、逆もしかり - 照明制御 在宅時の夕方に暗くなったらON、外出時と0時に強制OFF - エアコン制御 在宅時の活動時間中に寒かったらON、外出時と0時に強制OFF # 全体の構成 ![キャプションを入力できます](https://camo.elchika.com/bc70811f3406c21f15fd605daf45657bdd838c99/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64333139623236662d343133342d343733322d393339352d3264616365393631323762352f34333737326366372d326562612d346437662d613964632d646336323964636233613765/) # 材料 主なもの - [M5Stack](https://www.marutsu.co.jp/pc/i/1346016/) x1 - [M5AtomLite](https://www.marutsu.co.jp/pc/i/1634275/) x2 - [M5Stack用 LIGHTユニット](https://www.marutsu.co.jp/pc/i/1526328/) x1 - [M5Stack用 ENV.IIユニット](https://www.marutsu.co.jp/pc/i/1634663/) x1 - [MAMORIO](https://mamorio.jp/) x1 他に使ったもの - [M5Stack PLUSモジュール](https://www.marutsu.co.jp/pc/i/1526343/) (Groveポート拡張用、GPIO使えば不要) - [M5Stack GoPlusモジュール](https://www.marutsu.co.jp/pc/i/1632331/) (リモコン解析用、赤外線受信できれば何でも) - [赤外線送信機](https://www.digikey.jp/products/ja?keywords=101020026) (M5AtomLiteのが弱いので補助的に) - USBケーブル (電源用) # 設置と動作の様子 通常の時計表示とセンサーユニット ![前面](https://camo.elchika.com/4003f272f7bcfdaf3b1bf33d9313770014542765/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64333139623236662d343133342d343733322d393339352d3264616365393631323762352f30653231326537612d653764312d343939342d396133312d386630653835363861393266/) 諸々の開発機も兼ねているので、拡張モジュールを常時つけっぱなし ![側面](https://camo.elchika.com/46a034acbb678cd7a43610bdc36d41b95d202661/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64333139623236662d343133342d343733322d393339352d3264616365393631323762352f30663436366234662d353939302d346263382d383064632d373531643965316534386431/) ポモドーロタイマーは集中を削がないようシンプルに表示 ![ポモドーロ中と休憩中](https://camo.elchika.com/06ac8fc2834f6a44ba1b050edb8fe744d6034809/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64333139623236662d343133342d343733322d393339352d3264616365393631323762352f33643763653430322d343761662d343430312d626431392d656335643862636664333963/) リモコンはロフトベッドの枠に設置 ![もうちょっときれいにつけよう](https://camo.elchika.com/1855a917ef3ea1d0c24fcf8529f9f5c1a3f8b532/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64333139623236662d343133342d343733322d393339352d3264616365393631323762352f32613131613336392d353462312d346131632d616565392d303065653063353466653061/) MAMORIOはキーホルダーに ![aが小文字って今気づいた](https://camo.elchika.com/fa9273138c9c929d22e951b190f740aa24075829/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64333139623236662d343133342d343733322d393339352d3264616365393631323762352f38343863396562362d613061642d343363662d613637302d643964333435613765313063/) # プログラム ## リモコン側(M5AtomLite) リモコンの信号は、スケッチ例 > IRremoteESP8266 > IRrecvDumpV3​ を使って解析しました。 エアコン用は以下を変えるだけ。 - MyDeviceIdを1に - エアコン用の赤外線信号に - M5AtomLite内蔵の赤外線を使うならピン番号は12に ```arduino:照明用リモコンのプログラム #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入門」](http://www.ric.co.jp/book/contents/book_1209.html)を参考に。 ```arduino:本体のプログラム #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日の累積ポモドーロ数を表示したい

+

- ~~1日の累積ポモドーロ数を表示したい~~ 完了

- 環境センサの値をグラフで見たい - BLE通信中に時計の更新が止まるのでマルチタスクにしたい

-

- ボタン入力を割り込みにしたい

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