先日実装した摂取カロリー記録システムだが、既に紙での記録より便利なので2末を待たずに電子記録に完全移行した。
実はカロリー以外にも一週間の運動時間を紙に記録していたのだが、カロリー記録で味をしめたのでこちらも電子化したい。
単純にもう一台M5 Stackを買ってクライアントごと分けるということを思いついたけど、そんなことをせずともボタンの長押しを活用すれば1台でカバーできることに気づいた。
つまりカロリー記録モードと運動時間記録モードを長押しで切り替えるのである。
図示するとこんな感じ。
それで、開発を始めることにしたのだが、既存のM5 Stackはカロリー記録で本番稼働中なので一時的とはいえ開発中のプログラムを書き込んだり本番プログラムにロールバックしたりというのは面倒くさい。
本番稼働に影響を与えない形で開発したい。
というわけで、結局2代目ゲット。
外観が全く同じなので、テプラで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クライアントに影響を与えていないことを確認するためにテスト書き込みを行って、テストデータを消しておしまい。
以上