概要
リモートワークでだらけてしまうので、時間強制力が強めなポモドーロタイマーを作りました。
ついでに、毎日エアコンと照明をつけたり消したりするのが面倒なので、赤外線リモコンで自動化しました。
無駄に稼働しないように、自分が在宅中かどうかをBLEビーコン(今回はMAMORIO)で判別します。
機能
-
始業アラーム
朝9時に鳴って、止めると強制的にポモドーロタイマーがスタート -
ポモドーロタイマー
ポモドーロ終了のアラームを止めると強制的に休憩開始、逆もしかり -
照明制御
在宅時の夕方に暗くなったらON、外出時と0時に強制OFF -
エアコン制御
在宅時の活動時間中に寒かったらON、外出時と0時に強制OFF
全体の構成
材料
主なもの
- M5Stack x1
- M5AtomLite x2
- M5Stack用 LIGHTユニット x1
- M5Stack用 ENV.IIユニット x1
- MAMORIO x1
他に使ったもの
- M5Stack PLUSモジュール (Groveポート拡張用、GPIO使えば不要)
- M5Stack GoPlusモジュール (リモコン解析用、赤外線受信できれば何でも)
- 赤外線送信機 (M5AtomLiteのが弱いので補助的に)
- USBケーブル (電源用)
設置と動作の様子
諸々の開発機も兼ねているので、拡張モジュールを常時つけっぱなし
プログラム
リモコン側(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
さんが
2021/02/28
に
編集
をしました。
(メッセージ: 初版)
-
gri
さんが
2021/02/28
に
編集
をしました。
-
gri
さんが
2021/02/28
に
編集
をしました。
-
gri
さんが
2021/03/01
に
編集
をしました。
-
gri
さんが
2021/03/13
に
編集
をしました。
-
gri
さんが
2021/03/13
に
編集
をしました。
-
gri
さんが
2021/03/20
に
編集
をしました。
-
gri
さんが
2021/03/20
に
編集
をしました。
-
gri
さんが
2021/03/20
に
編集
をしました。
ログインしてコメントを投稿する