Alaのアイコン画像
Ala 2021年01月04日作成
セットアップや使用方法 セットアップや使用方法 閲覧数 1379
Ala 2021年01月04日作成 セットアップや使用方法 セットアップや使用方法 閲覧数 1379

Leafonyで角度計を作る

Leafonyで角度計を作る

1. はじめに

Leafonyのサンプルをいくつか試して、
Leafonyを用いて何か作品作りたいなと思いデジタル角度計を作成してみました。

実は前から作っていたのですが、詳しくはおわりににも書いてますが、角度の求めかたなど納得できていない部分があり投稿はしていなかったのですが、キャンペーンで盛り上がっているのでひとつ投稿してみようかなと記事にしました。

2. 作成内容

Leafonyを用いたデジタル角度計となります。

以下のようなことができます。

  • LCDリーフのボタン1(左ボタン)で角度をリセット(現在の傾きを0として設定)
    • ボタン押下した基準からの相対角度を求めるために使用します。
      角度計って相対角度とか求めれるよね?ということで要るかなと
  • LCDリーフのボタン2(右ボタン)で画面をポーズ
画面1 画面2 画面3
キャプションを入力できます キャプションを入力できます キャプションを入力できます

3. Leafの構成

リーフの構成は上から順に以下のように組み立ててネジで固定します。これでハードの準備は完了です。

※リンクは公式サイトのLeaf説明に飛びます。

4. プログラムの構成

4.1 フォルダ構成

VSCode + PlatformIO で作成します。

キャプションを入力できます

4.2 platformio.ini 設定

platformio.ini は以下のように記載します。

[env:pro8MHzatmega328]
platform = atmelavr
board = pro8MHzatmega328
framework = arduino
monitor_speed = 115200
lib_deps =
    # LCD Library
    tomozh/ST7032@0.0.0-alpha+sha.501bf64fe6
    # Adafruit LIS3DH
    https://github.com/adafruit/Adafruit_LIS3DH
    # MSTimer2
    https://github.com/PaulStoffregen/MsTimer2

monitor_speed はシリアルモニタでデバッグするために追記しています。

LCD Library は今回LCDに画面表示するために使用します。

Adafruit LIS3DH はセンサを使用するためのライブラリです。

MSTimer2 は割り込みタイマを使用するライブラリです。

4.3 main.cpp

4.3.1 Include Header

以下のようにplatformio.iniで追加したライブラリのヘッダ、I2C制御のWrire.hなどを記載します。

I2CFunc.h については、Leafonyのサンプルコード(https://docs.leafony.com/docs/examples/beginner/1_p/exten/lcd/) より、I2C制御関数を丸々使わせてもらっています。(4.4)参照。

#include <Arduino.h>
#include <ST7032.h>  // LCD
#include <Adafruit_LIS3DH.h>  // Sensor
#include <Wire.h>  // I2C
#include <I2CFunc.h>
#include <MsTimer2.h>

4.3.2 定数定義

Leafonyサンプル(https://docs.leafony.com/docs/examples/beginner/1_p/exten/lcd/ )を参考にIOピン、I2Cアドレスの定義を行っています。

/* 定数定義 */

#define SW1 2  // LCD SW1
#define I2C_EXPANDER_ADDR 0x1A  // LCD SW2
#define LOOP_INTERVAL 500 // 割り込み時間

4.3.3 プロトタイプ宣言

こちらについては処理内容は後述して説明します。

/* プロトタイプ宣言 */

void setup();
void loop();
float getTiltData();
float calcRelativeTilt(float tiltRaw);
void displayTiltData(float tilt);
void intervalTimer();

4.3.4 変数, オブジェクト

コメント記載の通りですが、相対角度を求めることができるように referenceTiltValue としてボタン1を押下時の角度を保持するための変数を定義しています。

isLcdUpdate はボタン2でポーズ機能を実装するために、画面の更新を行うかどうかのフラグを用意しています。

expireInterval についてはタイマ割り込みが起こったタイミングでフラグを立てて、次のメイン周期で処理を実行するためのフラグとなります。

/* 変数 */

// 傾きの基準値
// 指定位置からの相対角度を求めるために使用する
float referenceTiltValue;

// LCDの表示を更新するかどうか
bool isLcdUpdate = true;

// タイマ割り込みが起こったか
// 割り込み処理内で true、
// 周期処理内で false (Reset) に戻す
volatile bool expireInterval = false;


/* オブジェクト */

ST7032 lcd;  // Lcd
Adafruit_LIS3DH accel = Adafruit_LIS3DH();  // Sensor

4.3.5 関数

4.3.5.1 セットアップ

setup 関数は起動時に呼ばれる関数なので、こちらで各種初期設定を行っています。

// 起動時セットアップ
void setup() {

  pinMode(SW1, INPUT);

  Serial.begin(115200);  // シリアル通信
  Wire.begin();

  // IO Expander 初期設定
  i2c_write_byte(I2C_EXPANDER_ADDR, 0x03, 0xFE);
  i2c_write_byte(I2C_EXPANDER_ADDR, 0x01, 0x01);  // LCD Power on

  // Sensor 初期設定
  accel.begin(0x19);
  accel.setClick(0, 0);
  accel.setRange(LIS3DH_RANGE_2_G);
  accel.setDataRate(LIS3DH_DATARATE_10_HZ);

  // LCD 初期設定
  lcd.begin(8, 2);
  lcd.setContrast(30);
  lcd.clear();

  // 起動画面
  lcd.setCursor(0, 0);
  lcd.print("Protract");
  lcd.setCursor(0, 1);
  lcd.print("or");
  delay(1000);
  lcd.clear();

  // 割り込みタイマの設定、実行
  // 500ms毎に、傾き値のLCD表示を行うためのフラグ処理
  MsTimer2::set(LOOP_INTERVAL, intervalTimer);
  MsTimer2::start();
}
4.3.5.2 周期関数

周期処理では、以下の処理を行います。

  • 500ms毎に傾きを算出しLCD表示
  • SW1が押下された場合に押下したタイミングの傾きを基準値として保持
  • SW2が押下された場合に画面の更新を一時停止⇔再開
// 周期処理
void loop() {
  
  char sw1Value;  // SW1値
  char sw2Value;  // SW2値
  float tiltRaw;  // センサ生データ(傾き)
  float relativeTilt;  // 相対的な傾き値

  if (expireInterval) {
    expireInterval = false;

    // センサデータ取得
    tiltRaw = getTiltData();

    // 相対値算出
    relativeTilt = calcRelativeTilt(tiltRaw);

    // センサデータをLCDに表示
    displayTiltData(relativeTilt);
  }

  // SW1値取得
  sw1Value = digitalRead(SW1);
  // SW1押下時
  if (sw1Value != 1) {
    // ボタン押下箇所で基準値を設定する
    // 0度設定 = 相対角度を測定できるようにする
    tiltRaw = getTiltData();
    referenceTiltValue = tiltRaw;

    lcd.setCursor(0, 1);
    lcd.print("Reset!");
    delay(500); // TODO: SW押下を連続判定させない為の暫定処置

    isLcdUpdate = true;  // 基準設定時はPause解除
    lcd.clear();
  }

  // SW2値取得
  sw2Value = i2c_read_byte(I2C_EXPANDER_ADDR, 0x00);
  // SW2押下時
  if ((sw2Value & 0x02) != 0x02) {
    // 画面更新を一時停止⇔再開
    isLcdUpdate = !isLcdUpdate;
    if (isLcdUpdate) {
      lcd.clear();
    } else {
      lcd.setCursor(0, 1);
      lcd.print("Pause");
    }
    delay(500); // TODO: SW押下を連続判定させない為の暫定処置
  }
}

中身について記載します。

if (expireInterval) {
    expireInterval = false;
	// センサ関連の処理
  }

これは、500ms割り込み処理が呼ばれたタイミングで expireInterval を true にしそれを参照することで、500ms毎に処理を行うようにしています。

こちらはLeafonyサンプルの https://docs.leafony.com/docs/examples/advanced/1_p/exten/4-sensors_lcd/ を参考にさせていただきました。

最初は loop() 内の最後に delay(500) をいれていたのですが、これを行ってしまうと、各SWの押下判定が delay がかかっている間取得することができず取得漏れが起きるため、割り込みを用いて500ms周期処理を実現しました。

※ サンプル読んだときは何故 delay 使わずめんどくさそうなことしているんだろう…と思っていたのですが、自分で書いてやっと理解できました......。

// SW1値取得
  sw1Value = digitalRead(SW1);
  // SW1押下時
  if (sw1Value != 1) {
    // ボタン押下箇所で基準値を設定する
    // 0度設定 = 相対角度を測定できるようにする
    tiltRaw = getTiltData();
    referenceTiltValue = tiltRaw;

    lcd.setCursor(0, 1);
    lcd.print("Reset!");
    delay(500); // TODO: SW押下を連続判定させない為の暫定処置

    isLcdUpdate = true;  // 基準設定時はPause解除
    lcd.clear();
  }

次にこちらはSW1の値を digitalRead で読んで、押下時に処理を行っています。

delay(500); はあまり短い時間だと一度押したつもりが何度もボタン押下と判定されることを防ぐために500msとしています。

※ ボタンの前回値を保持しておき、OFF→ONの変化をもって判定した方がいいのでは?という思いがあり暫定処置とコメントに記載しています。今回はディレイ対応で事足りたのでディレイを入れています。

// SW2値取得
  sw2Value = i2c_read_byte(I2C_EXPANDER_ADDR, 0x00);
  // SW2押下時
  if ((sw2Value & 0x02) != 0x02) {
    // 画面更新を一時停止⇔再開
    isLcdUpdate = !isLcdUpdate;
    if (isLcdUpdate) {
      lcd.clear();
    } else {
      lcd.setCursor(0, 1);
      lcd.print("Pause");
    }
    delay(500); // TODO: SW押下を連続判定させない為の暫定処置
  }
}

こちらは同様にSW2押下時の処理となります。SW2押下時は「画面更新を一時停止⇔再開」の機能を実現するために、 isLcdUpdate = !isLcdUpdate;isLcdUpdate フラグを反転させて値を保持するようにしています。この値をセンサの値をLCDに表示する(displayTiltData()) で用います。

4.3.5.3 割り込み
// 割り込みタイマ
// expireInterval = true にし、
// 周期処理での傾き値のLCD表示処理を実行するフラグとして使用する
void intervalTimer() {
  expireInterval = true;
}

500ms毎の割り込みで、周期処理でのセンサデータのLCD表示関連の処理を行うかどうかのフラグを設定しています。

※ Leafonyのサンプルコード(https://docs.leafony.com/docs/examples/advanced/1_p/exten/4-sensors_lcd/)で割り込みにて使用している変数の宣言で volatileが使用されています。

こちらのサイト(https://nekosan0.bake-neko.net/library_timer.html) を見ると、割り込み関数と共用する変数は volatile を付ける必要があるようです。そのため、変数の定義箇所では volatile bool expireInterval = false; としています。

こういう場合、メイン側と割り込み関数側で共用する変数は、「volatile」属性というものを付けて宣言する必要が あります。

https://nekosan0.bake-neko.net/library_timer.html

4.3.5.4 センサから傾き値を取得

ここが正直微妙な個所です。

一つが、本来角度計として左右の動きだけを計測したいのですが、現状前後左右で角度がでてしまっています。ちょっと前後に傾けてしまっただけで期待していない値に変わってしまうのはダメだなあと思いつつ……。

もう一つが、LCDリーフを上として、右に倒したとき0~180度、左に倒したときに0~180度といった角度が算出されますが、相対角度を求めたいときに、右向きの90度か左向きの90度か判断付かず、相対角度算出ができない問題がありました。とりあえずの仮対応としてY軸の値との対応をみて、0~360の値としています。これもデバッグによる実測からの対応付けでやってしまっています。

3軸センサなので[x, y, z]軸をうまいこと使えばできる気がするんですが、いろんなサイトみてもちょっと物理的理屈が理解できていませんので仮対応となります。一応それらしくは動きます。

// センサから傾き値を取得する
// return value = 傾き
float getTiltData() {
  float dataY_g;  // Y軸
  float dataZ_g;  // Z軸
  // 3軸センサからのデータ取得
  // 参考: https://docs.leafony.com/docs/examples/advanced/1_p/exten/4-sensors_lcd/
  accel.read();
  dataY_g = accel.y_g;
  dataZ_g = accel.z_g;
  if(dataZ_g >= 1.0){
      dataZ_g = 1.00;
  } else if (dataZ_g <= -1.0){
      dataZ_g = -1.00;
  }

  // TODO: 暫定 右回転 0 ~ 360
  // 基準値からの角度を求めたいときに、
  // 右向きの90度か左向きの90度か判断付かず、相対角度算出ができない。
  // 仮対応として、Y軸の値との対応をみて、0~360の値としている。
  
  // デバッグ結果
  // 上
  // [x, y, z] = [-0.01, -0.03,  1.03]
  // 右45
  // [x, y, z] = [-0.02, -0.70,  0.75]
  // 右に90
  // [x, y, z] = [-0.02, -1.01,  0.06]
  // 180度(さかさま)
  // [x, y, z] = [-0.02, -0.02, -1.01]
  // 左45
  // [x, y, z] = [ 0.00,  0.69,  0.72]
  // 左に90
  // [x, y, z] = [-0.02,  0.97,  0.01]

  // [Y,Z] = [-, +] or [-, -] のとき0 ~ 180
  if (
    (dataY_g <= 0 && dataZ_g >= 0) || 
    (dataY_g <= 0 && dataZ_g <= 0)
  ) {
    return acos(dataZ_g) / PI * 180;
  }
  else {
    return 360 - ( acos(dataZ_g) / PI * 180 );
  }

  // TODO: 横向きで角度を測る想定だが、現在は前後に倒したときにも角度が出てしまう。横の動きのみを計測したい。
}
4.3.5.5 相対角度を算出

相対角度算出する関数が以下となります。こちらも同上の内容から仮対応となっています。

// センサの傾き値を受け取り、
// 基準となっているreferenceTiltValueからの相対角度を算出する
float calcRelativeTilt(float tiltRaw) {
  float processedData;
  if (referenceTiltValue > tiltRaw) {
    // TODO: 暫定 0 ~ 360データとしているため
    processedData = 360 - referenceTiltValue + tiltRaw;
  } else {
    processedData = tiltRaw - referenceTiltValue;
  }
  return processedData;
}
4.3.5.6 センサの傾きをLCDに表示
// センサの傾き値をLCDに表示
// ※ isLcdUpdate = trueの場合のみ表示を更新する
void displayTiltData(float tilt) {
  char tiltStr[4];  // LCD表示用文字列データ(傾き)
  
  // TODO: 必要? https://docs.leafony.com/docs/examples/advanced/1_p/exten/4-sensors_lcd/
  if (tilt < 3) {
    tilt = 0;
  }

  // 文字列整形
  dtostrf(
    tilt,    // 浮動小数点値
    8,       // 文字列長
    0,       // 小数点以下桁数
    tiltStr  // 文字列Buffer
  );
  Serial.print(tiltStr);

  // センサデータをLCDに表示
  if (isLcdUpdate) {
    lcd.setCursor(0, 0);
    lcd.print(tiltStr);
  }
}

float型の傾きのデータを dtostrf で文字列にしています。

またLCD表示は isLcdUpdate = true の場合のみとなるようにしています。

4.3.6 プログラム全体

コード全体を以下に記載します。

main.cpp

#include <Arduino.h>
#include <ST7032.h>  // LCD
#include <Adafruit_LIS3DH.h>  // Sensor
#include <Wire.h>  // I2C
#include <I2CFunc.h>
#include <MsTimer2.h>

/* 定数定義 */

#define SW1 2  // LCD SW1
#define I2C_EXPANDER_ADDR 0x1A  // LCD SW2
#define LOOP_INTERVAL 500 // 割り込み時間


/* プロトタイプ宣言 */

void setup();
void loop();
float getTiltData();
float calcRelativeTilt(float tiltRaw);
void displayTiltData(float tilt);
void intervalTimer();

/* 変数 */

// 傾きの基準値
// 指定位置からの相対角度を求めるために使用する
float referenceTiltValue;

// LCDの表示を更新するかどうか
bool isLcdUpdate = true;

// タイマ割り込みが起こったか
// 割り込み処理内で true、
// 周期処理内で false (Reset) に戻す
volatile bool expireInterval = false;


/* オブジェクト */

ST7032 lcd;  // Lcd
Adafruit_LIS3DH accel = Adafruit_LIS3DH();  // Sensor


/* 関数 */

// 起動時セットアップ
void setup() {

  pinMode(SW1, INPUT);

  Serial.begin(115200);  // シリアル通信
  Wire.begin();

  // IO Expander 初期設定
  i2c_write_byte(I2C_EXPANDER_ADDR, 0x03, 0xFE);
  i2c_write_byte(I2C_EXPANDER_ADDR, 0x01, 0x01);  // LCD Power on

  // Sensor 初期設定
  accel.begin(0x19);
  accel.setClick(0, 0);
  accel.setRange(LIS3DH_RANGE_2_G);
  accel.setDataRate(LIS3DH_DATARATE_10_HZ);

  // LCD 初期設定
  lcd.begin(8, 2);
  lcd.setContrast(30);
  lcd.clear();

  // 起動画面
  lcd.setCursor(0, 0);
  lcd.print("Protract");
  lcd.setCursor(0, 1);
  lcd.print("or");
  delay(1000);
  lcd.clear();

  // 割り込みタイマの設定、実行
  // 500ms毎に、傾き値のLCD表示を行うためのフラグ処理
  MsTimer2::set(LOOP_INTERVAL, intervalTimer);
  MsTimer2::start();
}

// 周期処理
void loop() {
  
  char sw1Value;  // SW1値
  char sw2Value;  // SW2値
  float tiltRaw;  // センサ生データ(傾き)
  float relativeTilt;  // 相対的な傾き値

  if (expireInterval) {
    expireInterval = false;

    // センサデータ取得
    tiltRaw = getTiltData();

    // 相対値算出
    relativeTilt = calcRelativeTilt(tiltRaw);

    // センサデータをLCDに表示
    displayTiltData(relativeTilt);
  }

  // SW1値取得
  sw1Value = digitalRead(SW1);
  // SW1押下時
  if (sw1Value != 1) {
    // ボタン押下箇所で基準値を設定する
    // 0度設定 = 相対角度を測定できるようにする
    tiltRaw = getTiltData();
    referenceTiltValue = tiltRaw;

    lcd.setCursor(0, 1);
    lcd.print("Reset!");
    delay(500); // TODO: SW押下を連続判定させない為の暫定処置

    isLcdUpdate = true;  // 基準設定時はPause解除
    lcd.clear();
  }

  // SW2値取得
  sw2Value = i2c_read_byte(I2C_EXPANDER_ADDR, 0x00);
  // SW2押下時
  if ((sw2Value & 0x02) != 0x02) {
    // 画面更新を一時停止⇔再開
    isLcdUpdate = !isLcdUpdate;
    if (isLcdUpdate) {
      lcd.clear();
    } else {
      lcd.setCursor(0, 1);
      lcd.print("Pause");
    }
    delay(500); // TODO: SW押下を連続判定させない為の暫定処置
  }
}

// 割り込みタイマ
// expireInterval = true にし、
// 周期処理での傾き値のLCD表示処理を実行するフラグとして使用する
void intervalTimer() {
  expireInterval = true;
}

// センサから傾き値を取得する
// return value = 傾き
float getTiltData() {
  float dataY_g;  // Y軸
  float dataZ_g;  // Z軸
  // 3軸センサからのデータ取得
  // 参考: https://docs.leafony.com/docs/examples/advanced/1_p/exten/4-sensors_lcd/
  accel.read();
  dataY_g = accel.y_g;
  dataZ_g = accel.z_g;
  if(dataZ_g >= 1.0){
      dataZ_g = 1.00;
  } else if (dataZ_g <= -1.0){
      dataZ_g = -1.00;
  }

  // TODO: 暫定 右回転 0 ~ 360
  // 基準値からの角度を求めたいときに、
  // 右向きの90度か左向きの90度か判断付かず、相対角度算出ができない。
  // 仮対応として、Y軸の値との対応をみて、0~360の値としている。
  
  // デバッグ結果
  // 上
  // [x, y, z] = [-0.01, -0.03,  1.03]
  // 右45
  // [x, y, z] = [-0.02, -0.70,  0.75]
  // 右に90
  // [x, y, z] = [-0.02, -1.01,  0.06]
  // 180度(さかさま)
  // [x, y, z] = [-0.02, -0.02, -1.01]
  // 左45
  // [x, y, z] = [ 0.00,  0.69,  0.72]
  // 左に90
  // [x, y, z] = [-0.02,  0.97,  0.01]

  // [Y,Z] = [-, +] or [-, -] のとき0 ~ 180
  if (
    (dataY_g <= 0 && dataZ_g >= 0) || 
    (dataY_g <= 0 && dataZ_g <= 0)
  ) {
    return acos(dataZ_g) / PI * 180;
  }
  else {
    return 360 - ( acos(dataZ_g) / PI * 180 );
  }

  // TODO: 横向きで角度を測る想定だが、現在は前後に倒したときにも角度が出てしまう。横の動きのみを計測したい。
}

// センサの傾き値を受け取り、
// 基準となっているreferenceTiltValueからの相対角度を算出する
float calcRelativeTilt(float tiltRaw) {
  float processedData;
  if (referenceTiltValue > tiltRaw) {
    // TODO: 暫定 0 ~ 360データとしているため
    processedData = 360 - referenceTiltValue + tiltRaw;
  } else {
    processedData = tiltRaw - referenceTiltValue;
  }
  return processedData;
}

// センサの傾き値をLCDに表示
// ※ isLcdUpdate = trueの場合のみ表示を更新する
void displayTiltData(float tilt) {
  char tiltStr[4];  // LCD表示用文字列データ(傾き)
  
  // TODO: 必要? https://docs.leafony.com/docs/examples/advanced/1_p/exten/4-sensors_lcd/
  if (tilt < 3) {
    tilt = 0;
  }

  // 文字列整形
  dtostrf(
    tilt,    // 浮動小数点値
    8,       // 文字列長
    0,       // 小数点以下桁数
    tiltStr  // 文字列Buffer
  );
  Serial.print(tiltStr);

  // センサデータをLCDに表示
  if (isLcdUpdate) {
    lcd.setCursor(0, 0);
    lcd.print(tiltStr);
  }
}

4.4. I2C 制御関数

I2CFunc.cpp, I2CFunc.hはI2C制御関数です。

ArduinoのWrie関数でI2Cの読み書きを行いますが、非常に使いやすいので、

Leafony サンプルコード(https://docs.leafony.com/docs/examples/beginner/1_p/exten/lcd/)から丸々 I2C制御関数をお借りしています。

I2CFunc.cpp

// I2C 制御関数
// Leafony サンプルコードより
// https://docs.leafony.com/docs/examples/beginner/1_p/exten/lcd/

#include <Wire.h>  // I2C
#include <I2CFunc.h>

//I2C スレーブデバイスに1バイト書き込む
void i2c_write_byte(int device_address, int reg_address, int write_data){
  Wire.beginTransmission(device_address);  // I2Cデバイスのアドレスを指定して送信処理の開始
  Wire.write(reg_address);  // まずレジスタ番号を指定
  Wire.write(write_data);
  Wire.endTransmission();  // 終了
}

//I2C スレーブデバイスから1バイト読み込む
unsigned char i2c_read_byte(int device_address, int reg_address){

  int read_data = 0;

  Wire.beginTransmission(device_address);
  Wire.write(reg_address);
  Wire.endTransmission(false);

  Wire.requestFrom(device_address, 1);  // デバイスに対し1バイトを要求
  read_data = Wire.read();

  return read_data;
}

I2CFunc.h

#ifndef I2CFunc
#define I2CFunc

//I2C スレーブデバイスに1バイト書き込む
void i2c_write_byte(int device_address, int reg_address, int write_data);

//I2C スレーブデバイスから1バイト読み込む
unsigned char i2c_read_byte(int device_address, int reg_address);

#endif  // I2CFunc

5. おわりに

これで一応は 2.作成内容 に示した内容ができました。

Leafonyのサンプルを色々動かしてみて、何か簡単なものを作りたいと思って角度の算出の仕方はわかるのだから角度計くらいなら簡単なのでは?と思って作り始めたものの角度計としては必要な相対角度を求めるのがうまくいかずに力業な解決になってしまいました。

今回実現はできたが正直微妙なので改善したいこととしては以下の点が挙げられます。

  • 相対角度の算出をうまいことやりたい
  • 前後左右の傾きで角度が出てしまうが、角度計としては前後の傾きは検出したくない
  • 角度算出の精度

物理的な内容理解が必要だなと思います...。

Alaのアイコン画像
電子工作初心者です 今はM5StickCとLeafonyで色々と遊んでます。
  • Ala さんが 2021/01/04 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する