M5Stack Japan Creativity Contest 2022向けに作成したM5stackスマート体重計の記事です。
ソースコードを晒すために記事を起こしておきます。
目次
1. 概要
シンプルなスマート体重計が欲しかったんです。
使うのにいちいちスマホアプリが必要なのはスマートじゃないので、
M5StackさんとGoogleさんの協力によって、
その場で体重の送信&トレンドの表示ができる体重計を作ってみました。
M5Stack Japan Creativity Contest 2022の紹介ページはこちら。
概要動画はこちら。
2. M5ソースコード
コンテストにあわせて突貫で作成したので、ちょいちょい余計な記述がありそうです。
体重の測定、GASへの送信、GASから返信された文字列データの数値と日数の変換、グラフ表示という内容です。
横軸日付、縦軸体重になるようにいい感じに座標変換してグラフ化表示関数を作りました。
日付もそれっぽく日数で横軸をあわせるようにしてます。
ただ1ヶ月を30日で計算してるので、
厳密には2月とか微妙な日数のときは若干ずれるはず。ほぼ気にならないレベルですが。
【転用時に変更するところ】
●wifi
ssidとpass 出先等での動作テストなどのため、Bボタンでスマホテザリングのssidにも変えられるようにしてます。
●GAS
デプロイしたIDをこちらに入力。
●id表示
spreadSheetに合わせてidを決めて、入力。
当然ですが、SpreadSheetに合わせないとGASスクリプト実行時にエラーになります。
下記ソースです。
長いので折りたたみます。
╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╮
╏ ソースコードを表示(折りたたみ) ╏
╰╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╯
#include "HX711.h"
#include <M5Stack.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>
// Wi-Fi
char* ssid="[ssid入力]"; //無線ルーターのssidを入力
char* password="[pass入力]"; //無線ルーターのパスワードを入力
char* ssid1="[ssid入力]"; //無線ルーターのssidを入力
char* password1="[pass入力]"; //無線ルーターのパスワードを入力
char* ssid2 = "[sub ssid入力]"; //無線ルーターのssidを入力
char* password2 = "[sub pass入力]"; //無線ルーターのパスワードを入力
int wifi_pos_x=100;//wifi表示の位置
int wifi_pos_y=5;//wifi表示の位置
int wifi_font_size = 2;//wifi表示の文字サイズ
int wifi_width=60;//wifi表示文字のサイズ
int wifi_height=15;//wifi表示文字のサイズ
char* wifi_list[2] = {"home","phone"};//接続先リスト home:家 phone:携帯テザリング
char* ssid_list[2] = {ssid1,ssid2};//ssidリスト
char* password_list[2] = {password1,password2};//パスワードリスト
int wifi_num = 0;//接続先リストの番号
//GAS
const char* server = "script.google.com";
const char* key ="[デプロイid入力]";
String adress = "https://script.google.com/macros/s/"; //googlescript web appのurl
String url=""; //url生成はSend関数で行う
String data=""; //データ生成はSend関数で行う
String response=""; //GASからのレスポンスを格納する変数
int send_font_size= 2;
int send_pos_x = 10;
int send_pos_y = 200;
int send_width = 100;
int send_height = 20;
//表示用のスプライト設定
TFT_eSprite sprite = TFT_eSprite(&M5.Lcd);
//pin設定
const int LOADCELL_DOUT_PIN = 2;
const int LOADCELL_SCK_PIN = 5;
//体重関係
HX711 scale;
float weight=0.0;
float inf=1000000000.0;
float max_weight=0.0;
float min_weight=inf;
int cnt_weight=10;//元300 体重安定確認の時間設定
float scale_calib=24.8934451534075*1000;//ここにcaribから算出した値を入れる
long scale_offset=-35207.9304914472;
int weight_limit = 200000;//前回体重とほぼ同じと判定するしきい値
int weight_font_size = 8;//測定した重量を表示する時の文字サイズ
int weight_pos_x=0;//体重表示の位置
int weight_pos_y=50;//体重表示の位置
int weight_width=330;//体重表示のサイズ
int weight_height=150;//体重表示のサイズ
int weight_sum;//体重の合計値
int pre_weight=0;//前回の体重値
int fix_time = 10000;//固定した体重を表示する時間(msec)
int pre_time;//現在時間
//ステータス表示
int status_pos_x=5;//ステータス表示の位置
int status_pos_y=25;//ステータス表示の位置
int status_font_size = 2;//ステータス表示の文字サイズ
int status_width=300;//ステータス表示のサイズ
int status_height=15;//ステータス表示のサイズ
char* measure_mode = "measurering...";//ステータス表示の文字
char* send_mode = "push send buttun";//ステータス表示の文字
char* sending = "now sending...";//ステータス表示の文字
char* send_complete = "send complete";//ステータス表示の文字
char* wifi_error = "wifi error";//ステータス表示の文字
char* sleep_mode = "sleep mode";//ステータス表示の文字
//グラフ表示
int graph_pos_x=5;
int graph_pos_y=5;
int graph_width=310;
int graph_height=210;
int data_max=0;
int data_min=100000;
String Date[100];
float Val[100];
int data_num=0;//送られてきたデータの数
//バッテリ
int battery_level;
int bat_pos_x=5;//バッテリ表示の位置
int bat_pos_y=5;//バッテリ表示の位置
int bat_font_size = 2;//バッテリ表示の文字サイズ
int bat_size_x=60;//バッテリ表示文字のサイズ
int bat_size_y=15;//バッテリ表示文字のサイズ
int sleep_time = 30000;//スリープまでの時間(msec)
//id表示
int id=2;
int id_max=3;
int id_pos_x=220;//idの位置(x)
int id_pos_y=5;//idの位置(y)
int id_width=70;//idのサイズ(x)
int id_height=15;//idのサイズ(y)
int id_font_size = 2;//idの文字サイズ
char* id_list[3] = {"[name1入力]","[name2入力]","[name3入力]"};//登録するid名
//id(左ボタン)
int id_btn_pos_x=50;//idのボタンの位置(x)
int id_btn_pos_y=220;//idのボタンの位置(y)
int id_btn_font_size = 2;//idのボタンの文字サイズ
int id_btn_width=30;//idのボタン文字の大きさ(x)
int id_btn_height=15;//idのボタン文字の大きさ(y)
int send_btn_pos_x=135;//sendのボタンの位置(x)
int send_btn_pos_y=220;//sendのボタンの位置(y)
int send_btn_width=50;//sendのボタン文字の大きさ(x)
int send_btn_height=15;//sendのボタン文字の大きさ(y)
int send_btn_font_size = 2;//sendのボタンの文字サイズ
//sleep(右ボタン)
int sleep_btn_pos_x=230;//sleepのボタンの位置(x)
int sleep_btn_pos_y=220;//sleepのボタンの位置(y)
int sleep_btn_width=60;//sleepのボタン文字の大きさ(x)
int sleep_btn_height=15;//sleepのボタン文字の大きさ(y)
int sleep_btn_font_size = 2;//sleepのボタンの文字サイズ
//カウンタ等
bool average_flag = false;
int cnt = 0;
bool send_flg = false;
void setup() {
ssid=ssid_list[wifi_num];
password=password_list[wifi_num];
M5.begin();
Serial.println("M5 start");
M5.Power.begin(); //バッテリ管理
M5.Power.setWakeupButton(BUTTON_C_PIN);
M5.update(); // ボタンの状態更新
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
scale.set_scale(scale_calib);
scale.set_offset(scale_offset);
max_weight = 0;
min_weight = 1000;
sprite.setColorDepth(8);
M5.Lcd.fillScreen(WHITE);
select_id();
pre_time = millis();
disp_default();
Serial.println("setup end");
}
void disp_default(){
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,measure_mode);
disp_text(id_btn_pos_x,id_btn_pos_y,id_btn_width,id_btn_height,id_btn_font_size,"ID");
disp_text(send_btn_pos_x,send_btn_pos_y,send_btn_width,send_btn_height,send_btn_font_size,"SEND");
disp_text(sleep_btn_pos_x,sleep_btn_pos_y,sleep_btn_width,sleep_btn_height,sleep_btn_font_size,"SLEEP");
disp_text(id_pos_x,id_pos_y,id_width,id_height,id_font_size,id_list[id]);
disp_text(wifi_pos_x,wifi_pos_y,wifi_width,wifi_height,wifi_font_size,wifi_list[wifi_num]);
}
void loop() {
M5.update(); // ボタンの状態更新
if(M5.BtnA.wasPressed()){
select_id();
}
get_weight();
if(weight<=0){
weight=0;
}
disp_weight(weight_pos_x,weight_pos_y,weight_width,weight_height,weight_font_size,weight);
if(M5.BtnC.wasPressed()){
M5.Power.deepSleep();
}
if(M5.BtnB.wasPressed()){
wifi_num++;
if(wifi_num>=2){
wifi_num=0;
}
ssid=ssid_list[wifi_num];
password=password_list[wifi_num];
Serial.println("ssid change");
Serial.println(ssid);
disp_default();
}
if(millis()-pre_time>sleep_time){
Serial.println("休止します");
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,sleep_mode);
delay(1000);
M5.Power.deepSleep();
}
}
void select_id(){
Serial.println("start select_id");
id++;
if(id>=id_max){
id=0;
}
M5.Lcd.setTextSize(id_font_size); //Set the font size.
M5.Lcd.setCursor(id_pos_x,id_pos_y); //文字表示の左上位置を設定
M5.Lcd.printf(id_list[id]);
disp_bat(bat_pos_x,bat_pos_y,bat_size_x,bat_size_y,bat_font_size);
pre_time = millis();
}
void disp_bat(int pos_x,int pos_y,int width,int height,int font_size){
battery_level = M5.Power.getBatteryLevel();
sprite.createSprite(width, height);
sprite.setCursor(0,0);
sprite.setTextFont(1);
sprite.setTextSize(font_size);
sprite.printf("%d %%",battery_level);
sprite.pushSprite(pos_x, pos_y);
sprite.deleteSprite();
Serial.print("bat:");
Serial.printf("%d %%",battery_level);
Serial.println("");
}
void get_weight(){
Serial.println("start get_weight");
Serial.println("cnt:"+String(cnt));
pre_weight=weight;
weight=scale.get_units(5);
max_weight = max(max_weight,weight);
min_weight = min(min_weight,weight);
if(cnt>=cnt_weight){
cnt=0;
Serial.println("weight_max:"+String(max_weight)+" weight_min:"+String(min_weight));
if(max_weight-min_weight<=weight_limit && min_weight>5){
fix_weight();
}
if(min_weight>5){
pre_time = millis();
}
max_weight = 0;
min_weight = 1000;
}
else{
cnt++;
}
Serial.println("weight:"+String(weight));
Serial.println("");
}
void disp_text(int pos_x,int pos_y,int width,int height,int font_size,char* text){
sprite.createSprite(width, height);
sprite.setCursor(0,0);
sprite.setTextFont(1);
sprite.fillScreen(BLACK);
sprite.setTextSize(font_size);
sprite.print(text);
sprite.pushSprite(pos_x, pos_y);
sprite.deleteSprite();
}
void disp_weight(int pos_x,int pos_y,int width,int height,int font_size,float num){
sprite.createSprite(width, height);
sprite.setCursor(0,0);
sprite.setTextFont(7);
sprite.setTextSize(3);
sprite.printf("%.1f",num);
sprite.pushSprite(pos_x, pos_y);
sprite.deleteSprite();
}
void fix_weight(){
Serial.println("start fix_weight");
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,send_mode);
for(int i=0;i<10;i++){//点滅表示
if(i%2==0){
M5.Lcd.fillRect(weight_pos_x,weight_pos_y,weight_width,weight_height,WHITE);
}
else{
M5.Lcd.fillRect(weight_pos_x,weight_pos_y,weight_width,weight_height,BLACK);
disp_weight(weight_pos_x,weight_pos_y,weight_width,weight_height,weight_font_size,weight);
}
pre_time = millis();
while(millis() - pre_time < 500){
M5.update();
if(M5.BtnA.wasPressed()){
select_id();
pre_time = millis();
i=0;
break;
}
if(M5.BtnB.wasPressed()){
disp_weight(weight_pos_x,weight_pos_y,weight_width,weight_height,weight_font_size,weight);
response = send_data_http(weight);
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,send_complete);
Serial.print("Response:");
Serial.println(response);
data_decode(response);
disp_graph(Date,Val,data_num);
delay(10000);
send_flg=true;
break;
}
}
if(send_flg==true){
send_flg=false;
break;
}
pre_time = millis();
}
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,measure_mode);
}
//https://www.1ft-seabass.jp/memo/2022/05/14/m5stack-airtable-api-connect-using-httpclient/
String send_data_http(float weight_val){
Serial.println("start send_data_http");
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,sending);
data=String("&1_cell=") + id_list[id] + String("&2_cell=") + weight_val;
url=adress + key + "/exec?";
Serial.println(url+data);
//wifi接続
Serial.println("wifi接続中");
WiFi.begin(ssid, password);
cnt=0;
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
cnt++;
if(cnt>10){
Serial.println("wifi接続失敗");
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,wifi_error);
delay(2000);
return "wifi接続失敗";
}
}
cnt=0;
Serial.println("wifi接続完了");
//http送信
HTTPClient http;
http.begin(url + data);
const char *headerNames[] = {"Location"}; // Locationをとる
http.collectHeaders(headerNames, sizeof(headerNames) / sizeof(headerNames[0]));
Serial.print("[HTTP POST] ...\n");
int httpCode = http.POST(data);
int status_code = http.GET();
Serial.printf("\npost request: status code = %d\n", httpCode);
Serial.printf("\nget request: status code = %d\n", status_code);
//status_codeは302が返ってくるので、それを参照して、リダイレクト先を取得する。
if (status_code == HTTP_CODE_FOUND){
//ペイロードを表示
String payload = http.getString();
Serial.println(payload);
// ヘッダのLocation(リダイレクト先URL)を取り出す
Serial.println("Location");
Serial.println(http.header("Location"));
// リダイレクト先にGetリクエスト
String redirect_url = http.header("Location");
http.begin(redirect_url); // HTTP
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK)
{
String res = http.getString(); //目的のレスポンスが得られる
Serial.println("GET RESPONSE!");
Serial.println(res);
return res;
}
}
}
//Stringから日付の配列:Date[]、数値の配列:Val[]、dataの数:data_numへ入力する
void data_decode(String res){
Serial.println("start decode");
String response=res;
int len =response.length();
String str="";
data_num=0;
cnt=0;
String D_loc[100];
float V_loc[100];
for(int i=0;i<len;i++){
if(response[i]==','){
if(cnt==0){
D_loc[data_num]=str;
cnt++;
}
else if(cnt==1){
V_loc[data_num]=str.toFloat();
data_max=max(data_max,int(V_loc[data_num]));
data_min=min(data_min,int(V_loc[data_num]));
cnt=0;
data_num++;
}
str="";
}
else{
str=str+res[i];
//最後だけコンマ検出ないので個別で入力しておく
if(i==len-1){
Serial.println("last");
V_loc[data_num]=str.toFloat();
data_max=max(data_max,int(V_loc[data_num]));
data_min=min(data_min,int(V_loc[data_num]));
data_num++;
break;
}
}
}
cnt=0;
Serial.println("data_num:"+String(data_num));
for(int i=0;i<data_num;i++){
Date[i]=D_loc[data_num-1-i];
Val[i]=V_loc[data_num-1-i];
}
for(int i=0;i<data_num;i++){
Serial.print(String(i));
Serial.print(":");
Serial.print(Date[i]);
Serial.print(",");
Serial.println(Val[i]);
}
}
void disp_graph(String a[],float Val[],int data_num){
Serial.println("start disp_graph");
sprite.createSprite(graph_width, graph_height);
Serial.println("color depth:"+String(sprite.getColorDepth()));
sprite.fillScreen(BLUE);
sprite.setTextColor(WHITE);
sprite.setTextFont(1);
sprite.setTextSize(2);
int first_x=30;//グラフ枠の左端x座標
int last_x=graph_width-5;//グラフ枠の右端x座標
int graph_flame_width=graph_width-35;
int graph_flame_height=graph_height-30;
int graph_max=int(data_max/5+1)*5;//max値を5の倍数で切りよくする
int graph_min=int(data_min/5)*5;//min値を5の倍数で切りよくする
int graph_step=graph_flame_width/(data_num-1);//グラフのx軸のステップ
//グラフの枠線
sprite.drawFastHLine(first_x,5,graph_flame_width,WHITE);
sprite.drawFastHLine(first_x,5+graph_flame_height,graph_flame_width,WHITE);
sprite.drawFastVLine(first_x,5,graph_flame_height,WHITE);
sprite.drawFastVLine(first_x+graph_flame_width,5,graph_flame_height,WHITE);
//グラフの目盛り
sprite.setCursor(0,5);
sprite.print(graph_max);
sprite.setCursor(0,graph_flame_height-10);
sprite.print(graph_min);
sprite.setCursor(5,10+graph_flame_height);
sprite.print(Date[0]);
sprite.setCursor(first_x+graph_flame_width-40,10+graph_flame_height);
sprite.print(Date[data_num-1]);
//データのトータル日数を計算
int start_day=culc_days(Date[0]);//データの最初の日付シリアル的な値
int end_day=culc_days(Date[data_num-1]);//データの最後の日付シリアル的な値
int total_div_days=end_day-start_day;//データの最後の日付シリアル的な値と最初の日付シリアル的な値の差
float graph_step_div=float(graph_flame_width)/float(total_div_days);//1日あたりのグラフのx軸のステップ
int pre_x=first_x;
for(int i=0;i<data_num-1;i++){//iをgraph_idx[i]にしてデータを抽出
// int x=10+graph_step*(data_idx);//プロットするx座標
float div_days=culc_days(Date[i])-start_day;//最初のデータからの日数
int x=first_x+float(div_days*graph_step_div);//プロットするx座標
int y=10+int((graph_max-Val[i])*graph_flame_height/(graph_max-graph_min));
//次の点の情報
float div_days1=culc_days(Date[i+1])-start_day;//最初のデータからの日数
int x1=first_x+graph_flame_width*float(div_days1/total_div_days);//プロットするx座標
int y1=10+int((graph_max-Val[i+1])*graph_flame_height/(graph_max-graph_min));
sprite.drawLine(x,y,x1,y1,RED);//線を引く
sprite.fillCircle(x, y, 5, RED); //塗りつぶし center-x, center-y, radius
sprite.fillCircle(x1, y1, 5, RED); //塗りつぶし center-x, center-y, radius
last_x=305;
if(x-pre_x>50 && (last_x-40)-x>80){
pre_x=x;
sprite.setTextSize(2);
sprite.setCursor(x,10+graph_flame_height);
sprite.print(Date[i]);
sprite.setTextSize(1);
sprite.setCursor(x,y);
sprite.printf("%.1f",Val[i]);
sprite.pushSprite(graph_pos_x,graph_pos_y);
}
}
Serial.println("graph plot end");
sprite.pushSprite(graph_pos_x,graph_pos_y);
sprite.deleteSprite();
delay(20000);
pre_time=millis();
M5.Lcd.fillScreen(WHITE);
disp_default();
disp_weight(weight_pos_x,weight_pos_y,weight_width,weight_height,weight_font_size,weight);
}
void send_data(int data){
Serial.println("start send_data");
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,sending);
//送信用のurlを作成
url=adress + key + "/exec?" + "&1_cell="+id_list[id]+"&2_cell="+weight;
Serial.println(url);
//wifi接続
Serial.println("wifi接続中");
WiFi.begin(ssid, password);
cnt=0;
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
// wait 1 second for re-trying
delay(1000);
cnt++;
if(cnt>10){
Serial.println("wifi接続失敗");
disp_text(status_pos_x,status_pos_y,status_width,status_height,status_font_size,wifi_error);
delay(2000);
return;
}
}
cnt=0;
Serial.println("wifi接続完了");
WiFiClientSecure client;
// サーバーにアクセス
Serial.println("サーバーに接続中...");
client.setInsecure();//認証をパスする
//データの送信エラー時
if (!client.connect(server, 443)) {
Serial.println("接続に失敗しました");
Serial.println("");//改行
return;
}
Serial.println("サーバーに接続しました");
client.println("GET " + url);
delay(3000);
client.stop();
Serial.println("送信完了");
pre_time = millis();
}
int culc_days(String s){
int len=s.length();
String m="";
String d="";
int pos=s.indexOf("/");
m=s.substring(0,pos);
d=s.substring(pos+1,len);
int days=m.toInt()*30+d.toInt();
return days;
}
3. GASソースコード
転用時はSheetのidを入力します。
M5からhttpでidと体重データがpostされたら、まずデータをid指定されたSheetへ書き込みます。
その後60日分のデータを検索し、データ数が20個になるように飛ばしながら日付と体重値を抽出し、
StringでM5へリターンします。
Stringから日付、数値への復号は前述の通りM5の方で実施してます。
こちらでグラフ化して、そのグラフ画像データを送信する、というような方式もあるんでしょうかね。
var id = '[Sheetのidを入力]';//スプレッドシートの指定
function doGet(e) {
console.log("start func_doget(e)")
console.log("check e:",e);
// 今日の日付
var d =new Date();
var date_t = Utilities.formatDate(d, 'Asia/Tokyo','yyyy/MM/dd');
var rowData = [];
rowData[0] = new Date(); //タイムスタンプ入力
for (var param in e.parameter) {
var value = e.parameter[param]; //送信されたデータを取得
rowData[parseInt(param)] = value; //配列に入れ込み
}
var name = rowData[1];
// console.log("name:",name);
//書き込み
var sheet = SpreadsheetApp.openById(id).getSheetByName('all');
var newRow = sheet.getLastRow() + 1; // 次の行に入力する
var newRange = sheet.getRange(newRow, 1, 1, rowData.length);//書き込みセルを指定。lengthでデータ個数を返してる?
newRange.setValues([rowData]); //ここでシートに書き込み
var sheet = SpreadsheetApp.openById(id).getSheetByName(name);
var newRow = sheet.getLastRow() + 1; // 次の行に入力する
var newRange = sheet.getRange(newRow, 1, 1, rowData.length);//書き込みセルを指定。lengthでデータ個数を返してる?
newRange.setValues([rowData]); //ここでシートに書き込み
//探索先のシートを設定
var weight_log=[];
var pos_Row = sheet.getLastRow(); //最終行の位置を取得
var last_date =sheet.getRange(pos_Row, 1).getValue();
var cur_date;
//一度にSheetデータを取り込んで配列に入れてから検索をかける
var all_data = sheet.getSheetValues(2,1,pos_Row-1,3);//全データの取得
var all_len=all_data.length;
for(let i=all_len-1;i>=all_len-100;i--){
if(i>all_len){
console.log("data nan");
break;
}
weight_log.push([all_data[i][0],all_data[i][2]]);
cur_date=all_data[i][0];
var dif_days=(last_date-cur_date)/ 1000 / 60 / 60 / 24;//最終行との日数差をシリアル値からの計算
if(dif_days>60){//60日以上経過していたらBreak
console.log("over 60 days");
break;
}
}
var len=weight_log.length;
var send_data=[];
var data_num=20-1;//データ数20個から最初と最後のデータを抜いた総データ数
var data_step=len/data_num;
for(let i=0;i<data_num;i++){
if(len<data_num){
send_data=weight_log;
break;
}
var pos=Math.round(data_step*i);
send_data.push([Utilities.formatDate(weight_log[pos][0], 'JST', 'M/d'),weight_log[pos][1]])
if(i==data_num-1){
send_data.push([Utilities.formatDate(weight_log[len-1][0], 'JST', 'M/d'),weight_log[len-1][1]]);//最後のデータを入れる
}
}
var res = send_data.toString();
console.log("res:",res);
return ContentService.createTextOutput(res);
}
4. Spreadsheet
最後にSpreadsheetです。
細かい点は割愛しますが、前述の通り、Sheet名=M5からのidになるようにSheet名をつけます。
allのSheetはあってもなくてもいいですが、全データがあるとなにかといいかなと思って一応おいてます。
いらんかったらSheet消してGASからもallに書き込む行を消しとけばいいです。
いじょう