t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

M5 Stack Basic ボタン長押しで動作モード変更するプログラム

先日実装した摂取カロリー記録システムだが、既に紙での記録より便利なので2末を待たずに電子記録に完全移行した。
実はカロリー以外にも一週間の運動時間を紙に記録していたのだが、カロリー記録で味をしめたのでこちらも電子化したい。

単純にもう一台M5 Stackを買ってクライアントごと分けるということを思いついたけど、そんなことをせずともボタンの長押しを活用すれば1台でカバーできることに気づいた。

つまりカロリー記録モードと運動時間記録モードを長押しで切り替えるのである。
図示するとこんな感じ。
f:id:t-hom:20210223210248p:plain

それで、開発を始めることにしたのだが、既存のM5 Stackはカロリー記録で本番稼働中なので一時的とはいえ開発中のプログラムを書き込んだり本番プログラムにロールバックしたりというのは面倒くさい。
本番稼働に影響を与えない形で開発したい。

というわけで、結局2代目ゲット。
f:id:t-hom:20210223211546p:plain

外観が全く同じなので、テプラでPRD・DEVシールを張って見分けるようにした。

M5 Stackのコード

力業で機能拡張したので褒められた内容ではないけど、とりあえず掲載。

//Under Development
#include <M5Stack.h>
#include <WiFi.h>

int p;
int digit[4];
char modeChr;

const char* ssid     = "xx999x-999x99-9";
const char* password = "99x999xx9999x";

const int port = 49153;
const IPAddress server_ip(192, 168, 1, 101);
WiFiClient client;

void setup() {
  // init lcd, serial, but don't init sd card
  M5.begin(true, false, true);
  M5.Power.begin();

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
  }
  modeChr = 'C';
  resetNumber();
}

// Add the main program code into the continuous loop() function
void loop() {
  // update button state
  M5.update();
 
  // if you want to use Releasefor("was released for"), use .wasReleasefor(int time) below
  if (M5.BtnA.wasReleased() || M5.BtnA.pressedFor(1000, 200)) {
    if(digit[p]++ >= 9){digit[p]=0;}
    displayNumber();
  } else if (M5.BtnB.wasReleased() || M5.BtnB.pressedFor(1000, 200)) {
    if(++p >= 4){p=0;}
    displayNumber();
  } else if (M5.BtnC.wasReleased() || M5.BtnC.pressedFor(1000, 200)) {
    int x = 0;
    x = digit[0]*1000 + digit[1]*100 + digit[2]*10 + digit[3];
    
    bool result = sendData(x);
    if (result){
      M5.Lcd.println("Done!");
      modeChr = 'C';
    }
    else {
      M5.Lcd.println("Failed!");
    }
    soundFeedback(result);
    
    delay(1000);
    resetNumber();
  } else if (M5.BtnB.wasReleasefor(700)) {
    resetNumber();
  } else if (M5.BtnA.wasReleasefor(700)) {
    if (modeChr == 'C') {
      modeChr = 'E';
    } else {
      modeChr = 'C';
    }
    resetNumber();
  }
}

void soundFeedback(bool result){
    if (result){
      M5.Speaker.tone(659, 200);
      delay(200);
    }
    else {
      M5.Speaker.tone(440, 100);
      delay(100);
      M5.Speaker.mute();
      delay(100);
      M5.Speaker.tone(440, 100);
      delay(100);
      M5.Speaker.mute();
      delay(100);
      M5.Speaker.tone(440, 100);
      delay(100);
    }
    M5.Speaker.mute();
}

bool sendData(int n) {
  M5.Lcd.setTextSize(2);
  M5.Lcd.println("Connecting...");

  if (modeChr == 'C') {
    M5.Lcd.println("Sending " + String(n) + " kcal.");  
  } else {
    M5.Lcd.println("Sending " + String(n) + " mins.");  
  }
  
  if (!client.connect(server_ip, port)) {
      return false;
  }
  if (modeChr == 'C') {
    client.print("C " + String(n));
  } else {
    client.print("E " + String(n));
  }
  
  return true;
}

void resetNumber() {
  if(modeChr =='C') {
    p = 1;
  } else {
    p = 2;
  }
  for (int i=0; i<4; i++){
    digit[i] = {0};
  }
  displayNumber();
}

void displayNumber() {
  M5.Lcd.clear(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextSize(10);
  if (modeChr == 'C') {
    for(int i=0; i<4; i++) {
      if(i==p){M5.Lcd.setTextColor(YELLOW);}
      else {M5.Lcd.setTextColor(WHITE);}
      M5.Lcd.print(digit[i]);
    }
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(180, 18);
    M5.Lcd.println("kcal");
  } else {
    for(int i=0; i<4; i++) {
      if(i==p){M5.Lcd.setTextColor(RED);}
      else {M5.Lcd.setTextColor(WHITE);}
      M5.Lcd.print(digit[i]);
    }
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(180, 18);
    M5.Lcd.println("mins");
  }
}

サーバープログラムも1つで、送信する数値に1文字のモードコード(C=Calorie, E=Exercise)を付加することで書き込み先のファイルを判定させるようにした。
PythonでSplitしやすいようにコードと値の間はスペース区切りとしている。

開発中プログラムでは接続先ポートも変えて、万が一本番側のサーバーが稼働していてもつながらないようにした。

ラズパイ側のコード

こっちもまあ褒められたものではない。

import socket
import time
import csv
import os
from datetime import datetime

def meal_type(timestamp):
    if 0 <= timestamp.hour <= 10:
        return "Breakfast"
    elif 11 <= timestamp.hour <= 16:
        return "Lunch"
    else:
        return "Dinner"

def record_calorie(cal):
    file_path = '/home/pi/test_calorie.csv'
    with open(file_path,'a',newline='') as f:
        w = csv.writer(f)
        timestamp =  datetime.now()
        w.writerow([timestamp.strftime('%Y-%m-%d %H:%M:%S'), meal_type(timestamp), cal])
        print([timestamp.strftime('%Y-%m-%d %H:%M:%S'), meal_type(timestamp) ,cal])

def record_exercise(minutes):
    file_path = '/home/pi/test_exercise.csv'
    with open(file_path,'a',newline='') as f:
        w = csv.writer(f)
        timestamp =  datetime.now()
        w.writerow([timestamp.strftime('%Y-%m-%d %H:%M:%S'), minutes])
        print([timestamp.strftime('%Y-%m-%d %H:%M:%S'), minutes])

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(("", 49153))
    while True:
        s.listen(1)
        conn, addr = s.accept()
        try:
            data = conn.recv(16).decode('utf8').split()
            print(data)
            if data[0] == "C":
                record_calorie(int(data[1]))
            elif data[0] == "E":
                record_exercise(int(data[1]))
            time.sleep(1)
        except socket.error:
            pass
        except KeyboardInterrupt:
            conn.close()
            s.close()
        conn.close()

受信したデータをカンマで区切って、最初のデータがCかEかで書き込み先のファイルや書き込み内容を振り分けている。
とりいそぎ、うまくいったようなので集計してグラフ表示させるプログラムができたら本番運用に乗せられる。

最後今回の開発がPRDサーバーとPRDクライアントに影響を与えていないことを確認するためにテスト書き込みを行って、テストデータを消しておしまい。

以上

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。