gri が 2021年03月01日00時18分13秒 に編集
コメント無し
本文の変更
# 概要 リモートワークでだらけてしまうので、時間強制力が強めなポモドーロタイマーを作りました。 ついでに、毎日エアコンと照明をつけたり消したりするのが面倒なので、赤外線リモコンで自動化しました。 無駄に稼働しないように、自分が在宅中かどうかを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[2] << 8 | data[3]) == iBeaconId && (data[21] << 24 | data[22] << 16 |data[23] << 8 | data[24]) == MyMamorioId) { tagFound = true; } else if ((data[1] << 8 | data[0]) == MyManufacturerId && data[2] != seq && data[3] == MyDeviceId) {
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 "BLEDevice.h" #include <WiFi.h> #include <Wire.h> #include "Adafruit_Sensor.h" #include <Adafruit_BMP280.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, }; 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; Adafruit_BMP280 bme; BLEAdvertising *pAdvertising; uint8_t seq = 0; float tmp = 0.0; float hum = 0.0; float pressure = 0.0; uint8_t pomo_state = POMO_STATE_NONE; uint16_t secLeft = 0; uint16_t deg = 0; uint16_t darkness = 0; uint32_t ms = 0; uint8_t prev_h = 25; struct tm tm; bool isWorkStart = false; void setup() { M5.begin(); M5.Speaker.begin(); M5.Speaker.mute(); M5.Lcd.setBrightness(16); M5.Lcd.fillScreen(TFT_BLACK); Wire.begin(); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); } configTime(JST, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); BLEDevice::init("M5Stack"); BLEServer *pServer = BLEDevice::createServer(); pAdvertising = pServer->getAdvertising(); while (!bme.begin(0x76)){ M5.Lcd.println("Could not find a valid BMP280 sensor, check wiring!"); } } void loop() { M5.update(); if (isWorkStart) { if (M5.BtnA.wasPressed() || M5.BtnB.wasPressed() || M5.BtnC.wasPressed()) { // 始業アラームを止めると強制的にポモドーロタイマースタート isWorkStart = false; M5.Speaker.mute(); pomo_state = POMO_STATE_WORK; M5.Lcd.fillScreen(TFT_BLACK); M5.Lcd.fillEllipse(160, 120, 100, 100, TFT_RED); secLeft = WORK_SEC; } } if (M5.BtnA.wasPressed()) { M5.Speaker.mute(); M5.Lcd.fillScreen(TFT_BLACK); deg = 0; if (pomo_state == POMO_STATE_WORK) { pomo_state = POMO_STATE_REST; M5.Lcd.fillEllipse(160, 120, 100, 100, TFT_GREEN); secLeft = REST_SEC; } else { pomo_state = POMO_STATE_WORK; M5.Lcd.fillEllipse(160, 120, 100, 100, TFT_RED); secLeft = WORK_SEC; } } if (M5.BtnC.wasPressed()) { M5.Speaker.mute(); M5.Lcd.fillScreen(TFT_BLACK); pomo_state = POMO_STATE_NONE; } if (getLocalTime(&tm)) { // 1時間ごとに実行 int h = tm.tm_hour; if (prev_h != h) { prev_h = h; darkness = analogRead(kLightPin); pressure = bme.readPressure(); if(sht30.get()==0){ tmp = sht30.cTemp; hum = sht30.humidity; } if (h == 9) { M5.Speaker.setVolume(2); M5.Speaker.beep(); isWorkStart = true; } if (h >= 16 && darkness > LIGHT_THD) { sendBLECommand(DEVICE_LIGHT, 1); } else if (h < 5 && darkness <= LIGHT_THD) { sendBLECommand(DEVICE_LIGHT, 0); } if (h >= 6) { if (tmp < 20.0) { sendBLECommand(DEVICE_AC, 1); } } else { if (tmp > 20.0) { sendBLECommand(DEVICE_AC, 0); } } } 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%% %dhPa\r\n", tmp, hum, (int)(pressure/100)); break; case POMO_STATE_WORK: case POMO_STATE_REST: // ポモドーロ 残り時間表示 if (secLeft) { secLeft--; uint16_t sec_max = pomo_state == POMO_STATE_WORK ? WORK_SEC : REST_SEC; while (deg <= (sec_max - secLeft) * 360 / sec_max) { fillArc(160, 120, deg++, 1, 90, 90, 90, TFT_BLACK); } } else { M5.Speaker.setVolume(1); M5.Speaker.beep(); } break; } } delay(1000); } void setAdvData(BLEAdvertising *pAdvertising, uint8_t deviceId, uint8_t command) { BLEAdvertisementData oAdvertisementData = BLEAdvertisementData(); oAdvertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | LE General Discoverable Mode std::string strServiceData = ""; strServiceData += (char)0x06; // 長さ strServiceData += (char)0xff; // AD Type 0xFF; Manufacturer specific strServiceData += (char)0xff; // テスト用カンパニーID(下位バイト) strServiceData += (char)0xff; // テスト用カンパニーID(上位バイト) strServiceData += (char)seq; // シーケンス番号 strServiceData += (char)deviceId; // 送信先デバイス strServiceData += (char)command; // 操作番号 oAdvertisementData.addData(strServiceData); pAdvertising->setAdvertisementData(oAdvertisementData); } void sendBLECommand(uint8_t deviceId, uint8_t command) { setAdvData(pAdvertising, deviceId, command); pAdvertising->start(); delay(1000); pAdvertising->stop(); delay(1000); seq++; } int 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 - BLE通信中に時計の更新が止まるのでマルチタスクにしたい - ボタン入力を割り込みにしたい # まとめ 己の惰性を律する一方で、助長している気もします。 QoLは上がりました。 M5Stackは楽しすぎて、無限増殖中です。