t-hom’s diary

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

Python: CSVデータをpandasで加工してmatplotlibでグラフ化する

今回もカロリー記録システムの開発記録である。
実は本日2本目の記事となる。なんとなく筆が乗ったというか、vimが乗ったというか、一気に開発を進めてしまった。

前回はカロリーをグラフ表示させるためのmatplotlib出力部分を作った。
thom.hateblo.jp

今回はこのグラフにデータを受け渡す部分。恐らく今回の開発で最難関になるだろう部分に取り組んだ。
pandasというライブラリでcsvからデータを読み取ってごにょごにょするんだけど、そのごにょごにょが凄く難しい。
単にpandasに慣れ親しんでいないためというのもあるけど、前回も体重記録を使えるデータに変換するのに大変に苦労した覚えがある。

コード

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, date, timedelta
df = pd.read_csv("/home/pi/calorie.csv", header=None, names=["timestamp","meal_type", "calorie"], parse_dates=["timestamp"])

print('\n--print dataframe which generated from csv--')
print(df)

dummy_data = []
for i in range(0, 7)[::-1]:
    for meal in ["Breakfast","Lunch","Dinner"]:
        sublist = []
        sublist.append((datetime.now()-timedelta(days=i)))
        sublist.append(meal)
        sublist.append(0)
        dummy_data.append(sublist)

dummy_columns = ["timestamp","meal_type","calorie"]
dummy_df = pd.DataFrame(data=dummy_data,columns=dummy_columns)

print('\n--print dummy dataframe for data completeness--')
print(dummy_df)

df = df.append(dummy_df,ignore_index=True)

df["date"] = df["timestamp"].dt.strftime("%Y-%m-%d")
df = df.groupby(["date","meal_type"]).agg({"calorie":"sum"}).reset_index()

print('\n--print completed data--')
print(df)

df = df.pivot_table(index="date", columns="meal_type", values="calorie")
print('\n--print pivot data--')
print(df)

print('\n--print tailed data--')
df = df.tail(7)
print(df)

record_date = [datetime.strptime(d,"%Y-%m-%d").strftime("%m/%d") for d in df.index]
minimum = np.array([1600 for i in df.index])
breakfast = df["Breakfast"].values
lunch = df["Lunch"].values
dinner = df["Dinner"].values
maximum = np.array([2400 for i in df.index])
 
plt.title("Calorie Record", fontsize = 22)
plt.xlabel("Date", fontsize = 22)
plt.ylabel("Calorie", fontsize = 22)
plt.grid(True)
 

plt.bar(record_date, maximum-minimum, width=0.25, bottom = minimum, tick_label = record_date, align="center", label="Guideline", color = "#98fb98", edgecolor="#008000", lw=0, hatch="/////")

plt.bar(record_date, breakfast, width=0.2, tick_label = record_date, align="center", label="Breakfast", color = "#f3d394")
plt.bar(record_date, lunch, width=0.2, bottom = breakfast, tick_label = record_date, align="center", label="Lunch", color = "#45938b")
plt.bar(record_date, dinner, width=0.2, bottom = breakfast+lunch, tick_label = record_date, align="center", label="Dinner", color = "#003a34")

#plt.legend(loc="upper right", fontsize=10)

plt.show()

決して褒められたコードではない。しかしなんとか動いたのでこれが今の限界。

動作イメージ

今日書いた別の記事と同じなので変わり映えしないけど、しいて言えば今日から電子的な記録を始めた実データを使った関係で前日までのデータは空っぽである。
f:id:t-hom:20210220232356p:plain

※朝ごはん(薄橙)が無いのは今朝クリニックで検査があった関係で食べてない為。

動作の詳細説明

このコードはデータが加工されていくそれぞれのステップでprintするように作っているので、そのステップごとのprint結果と何をしているのかを解説していく。

1) CSVから実際に記録されたカロリーデータを取得する

これは実データを読み込んでpandasライブラリのデータフレームという型になったものをprintしている。

--print dataframe which generated from csv--
            timestamp meal_type  calorie
0 2021-02-20 12:22:37     Lunch      498
1 2021-02-20 12:22:48     Lunch       51
2 2021-02-20 12:23:03     Lunch      456
3 2021-02-20 17:58:05    Dinner       84
4 2021-02-20 17:58:16    Dinner      321
5 2021-02-20 17:58:27    Dinner      108
6 2021-02-20 17:58:38    Dinner      765

2) 不足データを補完するためのダミーデータ作成

実データと同じ形式のDataFrameを過去7日分、それぞれBreakfast・Lunch・Dinnerを用意し、カロリーは0としておく。
集計されたときに項目の欠落を防止しつつ、実データと同じ日付でもカロリーに影響を与えない。

--print dummy dataframe for data completeness--
                    timestamp  meal_type  calorie
0  2021-02-14 23:11:14.317393  Breakfast        0
1  2021-02-14 23:11:14.317469      Lunch        0
2  2021-02-14 23:11:14.317488     Dinner        0
3  2021-02-15 23:11:14.317507  Breakfast        0
4  2021-02-15 23:11:14.317524      Lunch        0
5  2021-02-15 23:11:14.317541     Dinner        0
6  2021-02-16 23:11:14.317559  Breakfast        0
7  2021-02-16 23:11:14.317576      Lunch        0
8  2021-02-16 23:11:14.317593     Dinner        0
9  2021-02-17 23:11:14.317611  Breakfast        0
10 2021-02-17 23:11:14.317628      Lunch        0
11 2021-02-17 23:11:14.317644     Dinner        0
12 2021-02-18 23:11:14.317662  Breakfast        0
13 2021-02-18 23:11:14.317679      Lunch        0
14 2021-02-18 23:11:14.317696     Dinner        0
15 2021-02-19 23:11:14.317713  Breakfast        0
16 2021-02-19 23:11:14.317729      Lunch        0
17 2021-02-19 23:11:14.317746     Dinner        0
18 2021-02-20 23:11:14.317764  Breakfast        0
19 2021-02-20 23:11:14.317778      Lunch        0
20 2021-02-20 23:11:14.317793     Dinner        0

3) 実データのDataFrameとダミーのDataFrameを結合した後、dateとmeal_typeでグルーピング

--print completed data--
          date  meal_type  calorie
0   2021-02-14  Breakfast        0
1   2021-02-14     Dinner        0
2   2021-02-14      Lunch        0
3   2021-02-15  Breakfast        0
4   2021-02-15     Dinner        0
5   2021-02-15      Lunch        0
6   2021-02-16  Breakfast        0
7   2021-02-16     Dinner        0
8   2021-02-16      Lunch        0
9   2021-02-17  Breakfast        0
10  2021-02-17     Dinner        0
11  2021-02-17      Lunch        0
12  2021-02-18  Breakfast        0
13  2021-02-18     Dinner        0
14  2021-02-18      Lunch        0
15  2021-02-19  Breakfast        0
16  2021-02-19     Dinner        0
17  2021-02-19      Lunch        0
18  2021-02-20  Breakfast        0
19  2021-02-20     Dinner     1278
20  2021-02-20      Lunch     1005

4) meal_typeを列としてピボット集計する

--print pivot data--
meal_type   Breakfast  Dinner  Lunch
date                                
2021-02-14          0       0      0
2021-02-15          0       0      0
2021-02-16          0       0      0
2021-02-17          0       0      0
2021-02-18          0       0      0
2021-02-19          0       0      0
2021-02-20          0    1278   1005

5) 過去7日分だけ取り出す。

今回は実データが本日分しかないので加工結果は変わらない。
もしそれ以上のデータがあるとダミーもその分用意したり、グラフが細くなって見づらくなったりする。

--print tailed data--
meal_type   Breakfast  Dinner  Lunch
date                                
2021-02-14          0       0      0
2021-02-15          0       0      0
2021-02-16          0       0      0
2021-02-17          0       0      0
2021-02-18          0       0      0
2021-02-19          0       0      0
2021-02-20          0    1278   1005

6) numpyのndarray形式でそれぞれの列を抜き出してmatplotlibでplotする

ここは特に画面出力とかはしていない。
本当はDataFrameのままmatplotlibで出力する方法があるんだろうけど、前回の記事でnumpyのndarray形式をPlotするやり方の実績があるのでとりあえずデータを前回と同様の方式に合わせた。

CSVにカロリーを記録するプログラムの変更

以前書いたこちらの記事であるが、今回のプログラム作成にあたりこちらも変更した。
thom.hateblo.jp

これまで生のタイムスタンプだけ残してて、pandasで集計するときに時間によって朝食・昼食・夕食に分ければいいやと思ってたんだけど。。
そもそもデータ受信時に判定させて、一緒に記録しといたら楽なんじゃね?ということに気づいて改善。

カロリー記録時間をベースに以下の判定基準とした。
0時~11時:朝飯
11時~17時:昼飯
17時~24時:晩飯

深夜1時に何か食べるのが朝飯かと言われると微妙なんだが、当日の2時までは前日の晩飯扱いとかプログラム的に面倒なので。

一応コードも載せておこう。ブログに書いとけばぶっ飛んでも安心。(GitHub使えよといわれそうだけど。。)

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/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])

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(("", 49152))
    while True:
        s.listen(1)
        conn, addr = s.accept()
        try:
            data = conn.recv(16).decode('utf8')
            record_calorie(data)
            time.sleep(1)
        except socket.error:
            pass
        except KeyboardInterrupt:
            conn.close()
            s.close()
        conn.close()

おわりに

pandas、numpy、matplotlibはどれも非常に有用なツールであるが、私にとってはどれも非常に難しく感じる。
書籍が充実してなくて基本ネットで調べるしかないが、公式を当たると表記が専門的で難しく、有志のサイトを当たっても基礎知識がない状態でサンプルを見よう見まねで手探りで作っている状態だ。

でも以前に体重データ集計する際に使ったときよりは、pandasデータフレームについてずいぶん理解が進んだ気がする。

さて、今のところ摂取カロリー目安は固定にしているが、たぶん3か月もすればこの基準値では痩せなくなる。なぜなら体重の減少と同時に消費カロリーも減少するため。
そのあたりの計算は以下の記事で書いた。
thom.hateblo.jp

なので、次は摂取カロリー目安を体重記録と連動させて計算によって上下させる機能を追加したい。
あとはこのグラフをtkinterに埋め込むのと、カロリーデータ受信時に自動でグラフが起動するようにしたい。

既に運用でカバーできるくらいのところまでは作れたので、2月中はM5 Stackでの電子記録と紙記録を併用して、3月からは完全に電子記録に切り替えようと思う。

とりあえず、今回はここまで。

Python matplotlib で朝昼晩の食事カロリーをグラフ表示

今回も摂取カロリー記録システム開発の続き。
表示部分をmatplotlibというPythonのグラフライブラリを使って作っていく。

動作イメージ

実際の動作イメージはこんな感じ。
f:id:t-hom:20210220165014p:plain

積み上げ棒グラフで、下段から朝食、昼食、夕食の順にカロリーを表示させる。
斜線が入った薄い緑色のボックスはカロリー摂取目標を示していて、この範囲に届かないと基礎代謝を下回る無理なダイエット、この範囲を超えると食べすぎで痩せない。

今入れているデータは適当に設定したので実際のデータを使って表示させるのはもう少し先になる見通しである。

コード

import numpy as np
import matplotlib.pyplot as plt
 
record_date = np.array(["Feb.8", "Feb.9", "Feb.10", "Feb.11", "Feb.12", "Feb.13", "Feb.14"])
minimum = np.array([1600, 1600, 1600, 1600, 1600, 1600, 1600])
breakfast = np.array([400, 600, 700, 350, 700, 400, 450])
lunch = np.array([1000, 750, 1000, 1300, 600, 500, 350])
dinner = np.array([980, 800, 500, 500, 800, 800, 0])
maximum = np.array([2400, 2400, 2400, 2400, 2400, 2400, 2400])
 
plt.title("Calorie Record", fontsize = 22)
plt.xlabel("Date", fontsize = 22)
plt.ylabel("Calorie", fontsize = 22)
plt.grid(True)
 

plt.bar(record_date, maximum-minimum, width=0.25, bottom = minimum, tick_label = record_date, align="center", label="Guideline", color = "#98fb98", edgecolor="#008000", lw=0, hatch="/////")

plt.bar(record_date, breakfast, width=0.2, tick_label = record_date, align="center", label="Breakfast", color = "#f3d394")
plt.bar(record_date, lunch, width=0.2, bottom = breakfast, tick_label = record_date, align="center", label="Lunch", color = "#45938b")
plt.bar(record_date, dinner, width=0.2, bottom = breakfast+lunch, tick_label = record_date, align="center", label="Dinner", color = "#003a34")

plt.show()

分岐も繰り返しも使ってない為、全くインデントが無くてあんまりpythonらしくないコードになってしまった。

積み上げ棒グラフって面倒なイメージがあったけど、他のサイトを参考にコードをコピペしていじってるうちにさくっとできたので、matplotlibの強力さを改めて実感した。


今回のポイントは以下の部分。

plt.bar(record_date, maximum-minimum, width=0.25, bottom = minimum, tick_label = record_date, align="center", label="Guideline", color = "#98fb98", edgecolor="#008000", lw=0, hatch="/////")

plt.bar(record_date, breakfast, width=0.2, tick_label = record_date, align="center", label="Breakfast", color = "#f3d394")
plt.bar(record_date, lunch, width=0.2, bottom = breakfast, tick_label = record_date, align="center", label="Lunch", color = "#45938b")
plt.bar(record_date, dinner, width=0.2, bottom = breakfast+lunch, tick_label = record_date, align="center", label="Dinner", color = "#003a34")

上から順に、カロリー摂取目標、朝食、昼食、夕食をプロットしている。
ここでは積み上げグラフ機能を使っているわけではなく(そもそもあるのか知らない。たぶんない。)、単に普通の棒グラフの機能で、bottomを指定することで積み上げグラフを作っている。

だからdinnerのbottomはbreakfastとlunchを足した値となっていて、そこからdinner値分のグラフを描いているのだ。

一見面倒に思えるこのような指定は、カロリー摂取目安を描画するのに非常に役に立った。
つまり、bottomが自由に指定できるため、他の値に影響を受けずに任意の位置からデータを描画できるのだ。

今回は基礎代謝をminimumに、痩せるための最大摂取カロリー目安をmaximumに指定しているため、bottomがminimum、値はmaximum-minimum、Widthは他のグラフより少し太い0.25にすることでかぶさっても見えるようにした。

このあとの課題

グラフ表示の目安が付いたので、あとはこれに適したデータを準備してやる必要がある。
M5スタックから入力したカロリーデータは時刻・カロリーが1品1レコードの形式でcsv保存されている。
これを特定時刻を基準にして朝食・昼食・夕食の3種類に分類し、日付ごとのデータに分類しなければならない。
またしばらくpandasと格闘することになりそうだ。

あと、直近の体重データの最軽量値からカロリー摂取目安を計算するプログラムも必要になる。

それができれば、あとは体重変動グラフと摂取カロリーグラフを、どんなときに切り替えるかという課題もある。
体重を量った直後は体重グラフ、カロリー記録直後はカロリーグラフでいいんだけど、それ以外の待機時間にどっちも意識させたいのでボタンなどのUIでの任意切替かタイマー切替にしたい。

以上

ラズパイでソケット通信で受信したデータをCSVに書き込むPythonプログラム

今回も最近作り始めたカロリー記録システムの進捗紹介。

前回はM5 Stack Basicからラズパイにデータを送って画面表示するところまで作成した。
thom.hateblo.jp

今回はCSVにデータをOutputするところまで作成。地味ぃ。。

本当にやりたいのはMatplotlibでグラフ表示なんだけど、元データが集まらないとグラフも作りようがないし、適当なダミーで作り始めてもいつまでかかるか分からないので、それはそれで運用が始まらない。

ということでとりあえずは記録することから本運用を始めることにした。

動作中の画面

ネタも地味なら画面も地味。左上のターミナルがサーバーが受信時刻とデータを画面表示してるところで、右下のターミナルが記録されたcsvを表示したところ。
f:id:t-hom:20210219225648p:plain

コード

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

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

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(("", 49152))
    while True:
        s.listen(1)
        conn, addr = s.accept()
        try:
            data = conn.recv(16).decode('utf8')
            record_calorie(data)
            time.sleep(1)
        except socket.error:
            pass
        except KeyboardInterrupt:
            conn.close()
            s.close()
        conn.close()

まぁコードも特に大きな工夫はない。前回単純にprintしていたところをrecord_calorieという関数に置き換えてその中でCSV書き込みと画面出力を行っている。

以上。地味回でごめん。。

M5 Stack Basicでラズパイにデータ送信

前回摂取カロリー記録のための3ボタンUIを作ったので、今回はここに通信の仕組みを乗せていく。
thom.hateblo.jp

M5 Stackのコード

※ssidとパスワードは他人に見せられないので英字をすべてx、数字をすべて9に置き換えている。

#include <M5Stack.h>
#include <WiFi.h>

int p;
int digit[4];

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

const int port = 49152;
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);
  
  /*
    Power chip connected to gpio21, gpio22, I2C device
    Set battery charging voltage and current
    If used battery, please call this function in your project
  */
  M5.Power.begin();

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
  }
  
  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)) {
    /*M5.Lcd.setTextSize(2);
    M5.Lcd.println("Connecting...");
    delay(1000);*/
    int x = 0;
    x = digit[0]*1000 + digit[1]*100 + digit[2]*10 + digit[3];
    //M5.Lcd.println("Sending " + String(x) + " kcal.");
    //delay(1000);
    if (sendData(x)){
      M5.Lcd.println("Done!");
      M5.Speaker.tone(659, 200);
      delay(200);
    }
    else {
      M5.Lcd.println("Failed!");
      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();
    delay(1000);
    resetNumber();
  } else if (M5.BtnB.wasReleasefor(700)) {
    resetNumber();
  }
}

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

  M5.Lcd.println("Sending " + String(n) + " kcal.");
  if (!client.connect(server_ip, port)) {
      return false;
  }
  client.print(n);
  //client.stop();
  //WiFi.disconnect();
  return true;
}

void resetNumber() {
  p = 1;
  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);
  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");
}

M5 Stack側コードの説明

UIの挙動については前回の記事を参照して欲しい。
前回からの更新は、起動時にWifiに接続されることと、右ボタンでサーバーに接続してデータを送っている点である。

データ送信の度にサーバーに繋ぎに行くくせに切断するコードが無いことを不思議に思う方もいるかもしれない。
これはクライアント側から通信を切断すると上手く行かなかった為、試行錯誤した結果データを受信する度にサーバー側でコネクションをクローズさせるようにした為。
ネットワークの作法に疎いのでこれで良いのかあんまり分かってないんだけど、個人利用のプログラムだから安定稼働すればとりあえず良しとする。

あと送信失敗したときの音は以下のサイトのコードをそのままいただいた。
msr-r.net

ラズパイ側のコード

import socket
import time

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(("", 49152))
    while True:
        s.listen(1)
        conn, addr = s.accept()
        try:
            data = conn.recv(16)
            print(data)
            time.sleep(1)
        except socket.error:
            pass
        except KeyboardInterrupt:
            conn.close()
            s.close()
        conn.close()

ラズパイ側のコード解説

こちらは単純に受信データをPrintするだけのコード。
conn.recv(16)でデータを待ちつつデータが来たらプリントし、コネクションを切断するという流れ。
KeyboardInterruptを拾う位置がおかしい気がするので、いつかもう少し知識が付いたら書き直したい。

動作イメージはこんなかんじ。
f:id:t-hom:20210219003059p:plain

見てもつまらないとは思うけど、ここまでが一番の難所だった。
あとはファイルに記録してGUIで表示させれば、摂取カロリー記録システムの完成である。

ファイル記録は経験あるので問題ないけど、GUIは少々苦労しそうな気がする。

以上。

M5 Stack Basicで摂取カロリー記録用の3ボタンUI部分が完成

今回はM5 Stack Basicで摂取カロリー記録用の3ボタンUIが完成した。
動作画面はこんな感じ。
f:id:t-hom:20210217230325p:plain

動作イメージは動画でUpしようかと思ったんだけどGIF化してサイズ小さくしても収まらなかったので諦めた。。
ま、そんな大層なプログラムじゃないしな。

図示するとこんな感じ。
f:id:t-hom:20210217231347p:plain

桁移動は循環するようになっている。
単品で1000kcalを超えることが少ない為、入力桁の初期値は100の位としている。

他にも中央ボタン長押しで入力中の値をリセットする機能がある。

3ボタンという制約でシンプルに入力しやすさを工夫した結果として、ボタン数が少ないUIの典型的なパターンに収束した感じがする。

コードは以下のとおり。

#include <M5Stack.h>

int p;
int digit[4];

void setup() {
  M5.begin(true, false, true);
  M5.Power.begin();
  resetNumber();
}

// Add the main program code into the continuous loop() function
void loop() {
  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)) {
    M5.Lcd.setTextSize(2);
    M5.Lcd.println("Connecting...");
    delay(1000);
    int x = 0;
    x = digit[0]*1000 + digit[1]*100 + digit[2]*10 + digit[3];
    M5.Lcd.println("Sending " + String(x) + " kcal.");
    delay(1000);
    M5.Lcd.println("Done!");
    M5.Speaker.tone(659, 200);
    delay(200);
    M5.Speaker.mute();
    delay(1000);
    resetNumber();
  } else if (M5.BtnB.wasReleasefor(700)) {
    resetNumber();
  }
}

void resetNumber() {
  p = 1;
  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);
  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");
}

Cの配列を忘れてたり、添え字関連で未定義エリアを参照して画面が数値で埋め尽くされたりと色々とポカミスによる苦労はあったものの、なんとかそれっぽいプログラムになった。

なお、右の送信ボタンであるが、Connecting・Sending・Done!とプリントしてるだけで何もしてない。
このハリボテに後日ネットワーク機能を組み込んでラズパイに値を送付する予定。
実はM5 Stack側のネットワーク機能はできてるんだけど、ラズパイ側のサーバープログラムがまだ出来てない。
ということで本日はここまで。

以上。

M5 Stack Basicを初めてみた

前回の記事で今後ラズパイでやりたいこととして、「摂取カロリーの記録」を紹介した。
thom.hateblo.jp

それで、データをラズパイのメイン機に集めて常時可視化させるというところまでは決まっているが、その入力手段をどうするか検討してみた。

当初考えていた候補は、以下の6つ。

  • ラズパイをWebサーバーにしてWeb経由でInputさせる。
  • ラズパイでGUIを用意し、タッチスクリーンディスプレイで入力させる。
  • USBテンキーを接続して入力する。
  • キーマトリクスを繋いで入力する。
  • Arduinoで専用の入力デバイスを作成して、シリアル通信経由でラズパイに入力する。
  • ESP-WROOM-32で入力デバイスを作成し、WiFi経由でラズパイに入力する。

まぁ正直どれも実装が面倒だし、最初の2つなんてオペレーションも面倒くさい。
食事をとるために何か専用のアプリを起動なんてのは避けたい。

そこで目を付けたのがM5 Stack!

バッテリー・ディスプレイ・スピーカー・Micro SDカード・3ボタン(電源ボタン除く)・Wifi・Bluetooth・Grooveインターフェース・そしてもちろんGPIOピン。
これらが最初から1つのケースに収まっているので、わざわざいい感じのケースを3Dプリントする必要もない。

といっても例によって今回わざわざ購入したものではなく、「なんか面白そう」と思って去年買ったまま使う機会がなくて眠ってたデバイスである。

書籍を買って本格的に調べないと手を出せないと勝手に思い込んでたんだけど、とりあえずサンプルを動かすところまでは蛇腹折りのシンプルな説明書きで十分だった。
f:id:t-hom:20210215124950p:plain

サンプルさえ動いてしまえばArduinoと変わらないので勝ったようなもんだ。

システム構成としては、手元のM5 Stackでカロリー入力してディスプレイ表示させ、確定ボタンを押すとWifiでラズパイに飛ばすようなことを考えている。

まぁ3ボタンしかないのでデータ入力は色々考えないといけない。たとえば700キロの弁当なら、ボタン7回押すとかになると思う。
キーパッドを外付けするとまた配線を隠すケースが別途欲しくなるので3ボタンでなんとか作ろうと思う。

とりあえずいきなり目的物を作るのではなく、まずはモールス信号のような2値をラズパイに送るプログラムでも書いてみようかな。

そろそろ昼休みも終わるので今回の記事はこんなところで。

See you next time. Maybe.

私のラズパイメイン機の用途紹介・今後やりたいこと・開発管理で困ってることと打開策

今回は私がラズパイメイン機を何に使ってるのかざくっと紹介したい。
また、今後やりたいこととや開発管理で困っていること、その打開策等を紹介する。

興味はあるけど何に使えるのか分からないという方の参考になればと思う。

現在の用途

私は現在メインのラズパイ(3B+)を次の用途で使用している。

  • 体重測定
  • 室内環境測定
  • 定刻に行動を促す音声ガイダンス

体重測定は、乗ると音声で体重を読み上げてくれて、株価の変動みたいにローソク足チャートで変動を表示してくれるシステムを作成した。
thom.hateblo.jp


室内環境測定はArduinoで読み取った温度・湿度・気圧・PM2.5、CO2の値をUSBシリアル通信で受け取り、電光掲示板を担当するラズパイZeroにネットワーク経由で送信システムである。
thom.hateblo.jp
thom.hateblo.jp

音声ガイダンスは音声ファイルを用意しておいて、単にcron(自動実行の為のスケジューラー)からaplay(音声ファイルを再生するコマンド)で実行しているだけ。これはかなりシンプルなので特に紹介記事を書いたことはない。

今後ラズパイでやりたいこと

今後、というかまさに今やりたいことが、摂取カロリーの記録である。

在宅勤務がながびく中、最近また太ってきたのでそろそろダイエットを再開しないとまずい。最近は基本的に3食コンビニになってしまってるんだけど、一つだけ強みがある。それは、全品カロリーが書いてあるので計測できること。

それで、とりいそぎ紙で記録表を作ってみたのがこちら。
f:id:t-hom:20210214011441p:plain
日付ごとに、最初は△が並び、次に〇が並び、最後にまた△になっている。
100kcalごとに1マス、左から消し込んでいき、〇の範囲に収めれば自然にやせていくように計算して作った表である。
〇に届かずに前半の△で終わると基礎代謝を下回るので健康被害の可能性が出てくる・〇を超えて後半の△に差し掛かると痩せないもしくは太る。

これをラズパイでシステム化したい。

ちなみに摂取カロリー目安は体重とともに変動するので月に1回くらいは見直しが必要である。
その変動値を計算するマクロがこちら。
thom.hateblo.jp

ラズパイ上では既にPythonで直近の体重から摂取カロリー目安を割り出してくれるプログラムを作っているので、カロリー記録と目安カロリー計算の仕組みを連動させることができる。

開発管理で困ってること

開発そのものは色々調べながらやっていくしかないんだけど、今まさに困ってるのが開発の管理である。
具体的には成果物のデプロイ管理。

/home/pi/workの中身をご覧いただこう。
f:id:t-hom:20210214013552p:plain

作りかけのサンプルやバックアップファイルが散乱して結構カオスになってきている。
しかも何がまずいって、この中の「pandasample.py」が、本番運用されてる体重グラフ表示プログラムだってこと。。

体重グラフ表示の仕組みはこうだ。
1) 体重計から飛んでくる赤外線信号は常時Daemonで監視している。
2) Daemonの本体であるPythonプログラムが信号をキャッチすると音声で体重を読み上げ、ファイルに記録する。
3) Daemonは/home/pi/Desktop(は?)に置いてあるPlot.shをキックする。
4) Plot.shは/home/pi/work(は?)/pandasample.py(は?)をキックする。
5) pandasample.pyはグラフを表示させる。

他にもいくつか本番運用されてるpythonスクリプトがいかにも作業用であるworkフォルダに、いかにも検証用の名前で格納されてしまっている。
いや、されてしまったというか、私がやったんだけど。

ラズパイで運用を始めてからけっこう月日が経っているので既にどれが何のファイルなのか分からなくなっている。
もう一から綺麗に作り直したいけど、当時色々躓きながら一つずつ問題をクリアしてきたので、もう一度環境を再現できる自信がない。
一体どうすれば。。。

打開策

ということで打開策である。

単純に、開発・テスト機と本番運用機を分けることにした。
普段私が仕事でやっている管理である。

実はずいぶん前にこの結論にはたどり着いていて、ラズパイ3B+はもう1台持っているのだ。
ただ当時一緒に購入したタッチスクリーンディスプレイで間違えて5インチのものを買ってしまって使い勝手か微妙だったので放置していた。
今回は公式の7インチタッチスクリーンディスプレイを買ってきたので、本番環境と同じハード構成である。

ケースがメイン機より使い勝手良かったり、アダプターが足りないので実験用の可変電源でカバーしてたりするけど。。
f:id:t-hom:20210214015615p:plain
※テスターで測ってみたところ、USBからの出力はVOLTAGE設定にかかわらず常に5V強だった。そりゃそうか。普通のUSB機器は5V前提で設計されてるし可変にするメリットもない。

現行運用している本番機を参照しつつ、必要な環境だけを新しいラズパイに再現してそちらを本番機に切り替えるということを考えている。
また、環境の再現にあたって今回はきちんと変更管理を導入することにした。

実は購入時にも同じように途中までのセットアップできちんと変更を管理していたのだが、そのときの資料がこちら。
f:id:t-hom:20210214020051p:plain

ただ放置してしばらくしてまた実験的用途に使ってしまったので環境がよく分からなくなっていた。

今回はまたOSの再インストールからスタート。ただ前回の記録があるので前回試行錯誤してたところは今回スムーズに再現できた。
f:id:t-hom:20210214020258p:plain

Linuxは大体の設定作業がコマンドで片付くのでテキスト形式で記録を残しやすい。
変更を適切に記録しておくことで、次に同じ作業が必要になっても、本番運用環境がぶっ壊れても再現することができるのでおススメ。

これまでは再現性の低い奇跡的に出来上がった環境でなんとか動いていたようなものなので、これがぶっ壊れたらもう一度調べなおす気力は無かったと思う。
しかし自分で組んだこのシステムが既に生活に欠かせないものになってしまったので、もう1台分の費用をかけてでも安定化を図りたい。

今回なんとか、一番複雑な体重読み上げとグラフ表示の仕組みを新環境で再現することに成功したので、ひとまず今回の試みは成功だったと思う。

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