エスプレッソマシン支払いシステム作った話（通信・ハード編）

# はじめに
エスプレッソマシン支払いシステム作った話・第二弾では読み取ったQRコードの情報や認証情報の通信の話とハードウェアの話をしたいと思います。

第一弾
[エスプレッソマシン支払いシステム作った話（企画・QR読み取り編）](https://elchika.com/article/f27f4c0d-23db-400c-ae4a-e1a9916350cd/)

第三弾
[エスプレッソマシン支払いシステム作った話（サーバ・ウェブ編）](https://qiita.com/Prokuma/items/0d187e57ec990d18aa4a)

# 開発環境
Windows 11
Arduino 1.8.19

# セットアップ
- ESP32オリジナルボード
- Spresense
- デロンギ マグニフィカS ECAM23120
- フォトカプラ TLP4227G-2
- 抵抗

# 仕様
SpresenseがデコードしたQRコード情報をESP32に渡し、ESP32は受け取った情報をサーバーに転送します。サーバーから送られてきた認証情報を受け取り、Spresenseへと返しエスプレッソマシンの特定のボタンを一定時間有効にします。

# ボード間通信
サーバーに情報を送りたいため、Wifiが使用できるボードがあると便利なのですが、無いので作りました。
![Schematic_Solar system_2022-12-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969856/a1bb1a3d-ee5a-4280-5264-040aeb8af34a.png)
というのは冗談で、充電制御用に作ったボードが余ってたのでこれを使います。右上のU1の4つのピンに```SPI_MISO```、```SPI_MOSI```、```SPI_CS```、```SPI_SCLK```というポートが接続されていますが、これをSpresense側のピンに配線します。これでSpresenseとESP32間のSPI通信ができます。SpresenseがSPIマスター、ESP32をSPIスレーブとします。
![DSC_2811.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969856/0fcd699c-c64c-43ba-f832-2c83ecd28a04.jpeg)
元々SPIを使うつもりがなかったボードなので、変なところにワイヤをはんだ付けしてます。

### Spresense側SPIマスター
Spresenseは```VALID```か```FAILED```を受け取るまで一定時間```get```コマンドを送り続けます。
```cpp
#include <SPI.h>

//変数の設定
static const int MSG_SIZE = 44; //data size
uint8_t s_message_buf[MSG_SIZE]; //send buffer
uint8_t r_message_buf[MSG_SIZE]; //rev buffer

//SPI setting
SPISettings mySPISettings = SPISettings(6000000, MSBFIRST, SPI_MODE3);

void setup() {
  pinMode (SS, OUTPUT);  // start slave device
  SPI.begin();   // SPI init

  //buffer reset
  memset(s_message_buf, 0, MSG_SIZE);
  memset(r_message_buf, 0, MSG_SIZE); 
}

String datain;

void loop()
{
  datain.trim();                        // remove any \r \n whitespace at the end of the String
    
  //Write message to the slave
  for (int i = 0; i < MSG_SIZE; i++) {
    s_message_buf[i] = uint8_t (datain[i]);
  }

  //SPI start
  SPI.beginTransaction(mySPISettings);//start
  digitalWrite(SS, LOW); //start slave device

  //send rev
  for ( int i = 0; i < MSG_SIZE; i++) {
    r_message_buf[i] = SPI.transfer(s_message_buf[i]);  //send rev same time
    delayMicroseconds(50);//delay
  }
  digitalWrite(SS, HIGH);//end slave
  SPI.endTransaction();//SPI end
  delay(1000);
}
```



### ESP32側SPIスレーブ
ESP32はスレーブなので常にマスターのSpresenseの指示を待つことになります。基本的には正しいデコード情報がSpresenseから送られてきたらWifi転送フェーズへ移行し、サーバーでの検証結果により```VALID```か```FAILED```を返します。

```cpp
#include <ESP32DMASPISlave.h>

ESP32DMASPI::Slave slave;
static const int MSG_SIZE = 44;
uint8_t* s_message_buf;
uint8_t* r_message_buf;

String user_num_str;
String user_code;
int cup_num;
String revdata;
String senddata;
char revc[MSG_SIZE];

void setup() {
  // DMAバッファを使う設定　これを使うと一度に送受信できるデータ量を増やせる
  s_message_buf = slave.allocDMABuffer(MSG_SIZE); //DMAバッファを使う
  r_message_buf = slave.allocDMABuffer(MSG_SIZE); //DMAバッファを使う

  // 送受信バッファをリセット
  memset(s_message_buf, 0, MSG_SIZE);
  memset(r_message_buf, 0, MSG_SIZE);

  //送信データを作成してセット
  char *init_val = "WAIT";
  for (int i = 0; i < MSG_SIZE; i++) {
    s_message_buf[i] = uint8_t (init_val[i]);
  }
  slave.setDataMode(SPI_MODE3);
  slave.setMaxTransferSize(MSG_SIZE);
  slave.setDMAChannel(2); // 専用メモリの割り当て（1か2のみ)
  slave.setQueueSize(1); // キューサイズ　とりあえず1
  // HSPI(SPI2) のデフォルトピン番号は CS: 15, CLK: 14, MOSI: 13, MISO: 12
  slave.begin(HSPI,16,17,4,18); // 引数を指定しなければデフォルトのSPI（SPI2,HSPIを利用）
}

void loop() {
    for (int i = 0; i < MSG_SIZE; i++) {
      s_message_buf[i] = uint8_t(senddata[i]);
    }
      
      // キューが送信済みであればセットされた送信データを送信する。
    if (slave.remained() == 0) {
      slave.queue(r_message_buf, s_message_buf, MSG_SIZE);
      senddata = "WAIT";
    }
    
    // マスターからの送信が終了すると、slave.available()は送信サイズを返し、
    // バッファも自動更新される
    while (slave.available())
    {
      for (size_t i = 0; i < MSG_SIZE; ++i) {
        revc[i]=char(r_message_buf[i]);
      }
      revdata=revc;
      slave.pop();//finish transaction
      delay(500);      
      if(revdata.substring(40,41)=="1"||revdata.substring(40,41)=="2"){
            user_num_str = revdata.substring(0,28);
            user_code =  revdata.substring(29,39);
            cup_num = revdata.substring(40,41).toInt();
            senddata = callFunction(); //「サーバーとの通信」で説明
      }
      else if(revdata.substring(0,3)=="get"){
          Serial.println("GET");
      }
      else{
          senddata = "FAILED";
      }
      delay(500);
    }
    delay(500);
}
```

# サーバーとの通信
Wifi経由でGoogleのCloud Functionsを呼び出します。デコードされたQRコード情報はJSON形式で転送します。結果を```VALID```か```FAILED```で受け取ります。
```cpp
#include <WiFi.h>
#include <Firebase_ESP_Client.h>

//Provide the token generation process info.
#include "addons/TokenHelper.h"
//Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"

int counter = 0;

ESP32DMASPI::Slave slave;

////////////firebase
#define FIREBASE_PROJECT_ID "***YOUR_ID***"
#define FIREBASE_CLIENT_EMAIL "***YOUR_EMAIL***"
const char PRIVATE_KEY[]  = "-----BEGIN PRIVATE KEY-----\***KEY***\n-----END PRIVATE KEY-----\n";

/* 3. Define the project location e.g. us-central1 or asia-northeast1 */
// https://firebase.google.com/docs/projects/locations
#define PROJECT_LOCATION "us-central1"

#define USER_EMAIL "***@***" 
#define USER_PASSWORD "************" 

// Insert Firebase project API Key
#define API_KEY "****************************"

// Insert RTDB URLefine the RTDB URL */
#define DATABASE_URL "https://**********-default-rtdb.firebaseio.com" 

#define USER_EMAIL "******@******" 
#define USER_PASSWORD "**************" 

//Define Firebase Data object
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;

/* Function Prototype */
void connectToWifi();
void set_Firebase();
String callFunction();
 
// ルーター接続情報
#define WIFI_SSID "SSID"
#define WIFI_PASSWORD "PASSWORD"
#define HTTP_PORT 80

void setup() {
   connectToWifi();
   set_Firebase();
}

/* Wi-Fiルーターに接続する */
void connectToWifi() {
    WiFi.disconnect(true);  //disconnect form wifi to set new wifi connection
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        counter++;
        if(counter>=60){ //after 30 seconds timeout - reset board
            ESP.restart();
        }
    }
}

/* Firebaseの設定 */
void set_Firebase() {
    /* Assign the Service Account credentials */
    config.service_account.data.client_email = FIREBASE_CLIENT_EMAIL;
    config.service_account.data.project_id = FIREBASE_PROJECT_ID;
    config.service_account.data.private_key = PRIVATE_KEY;
    
    /* Assign the api key (required) */
    config.api_key = API_KEY;
    
    /* Assign the user sign in credentials */
    auth.user.email = USER_EMAIL;
    auth.user.password = USER_PASSWORD;

    /* Assign the RTDB URL */
    config.database_url = DATABASE_URL;
   
    /* Assign the callback function for the long running token generation task */
    config.token_status_callback = tokenStatusCallback; // see addons/TokenHelper.h

    Firebase.begin(&config, &auth);
    Firebase.reconnectWiFi(true);
    fbdo.setResponseSize(4096);
}

/* Cloud Functionsの呼び出し */
String callFunction()
{
    FirebaseJson payload;
    payload.setJsonData("{\"data\":{\"qrcode\":\"" + revdata + "\"}}");
     
    if (Firebase.Functions.callFunction(&fbdo, FIREBASE_PROJECT_ID /* project id */, PROJECT_LOCATION /* location id */, "test" /* function name */, payload.raw() /* data pass to Cloud function (serialized JSON string) */)){
        String p_fb = fbdo.payload().c_str();
        int f_valid = p_fb.lastIndexOf("VALID");
        if(f_valid>0)
          return p_fb.substring(f_valid, f_valid + 7);
        else
          return "FAILED";     
    }
    else
        return "FAILED";
}
```
# ボタンの有効化
Spresenseが無事にVALIDを受け取った場合、1杯か2杯かに応じて、エスプレッソマシンのボタンを有効にします。なるべくエスプレッソマシンの回路に干渉したくなかったので、Spresense側回路と絶縁するため、フォトカプラを使い以下のような回路を組んでいます。
![Schematic_coupler_2022-12-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969856/2c605cbc-327c-bd7a-7d9f-15ba94774716.png)
Input1、Input2には有効にしたいボタンに応じてHIGHかLOWの信号がSpresenseから送られてきます。データシートをよく見てなかったため、ノーマルクローズのフォトカプラを買ってしまいました。有効の時がLOWです。Port1-1、Port1-2はエスプレッソマシンのボタン1の直前の回路パターンをカットして、その間に挿入しています。全部で5個のボタンがあり、同様の回路が組んであります。

# 外観
プロトタイプはこんな感じです。ブレッドボードを使っているため、配線がごちゃごちゃしてますが、仕様が固まってきたらPCB化しようと思います。
![DSC_2815.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969856/612d2a30-d9b7-0f33-41d1-e31e31104f0b.jpeg)

# まとめ
エスプレッソマシン支払いシステム作った話（通信・ハード編）として、主にSPI通信、Wifiを使ったCloud Functionsの呼び出し、ボタンの有効化について書きました。次回は第三弾・[エスプレッソマシン支払いシステム作った話（サーバ・ウェブ編）](https://qiita.com/Prokuma/items/0d187e57ec990d18aa4a)としてサーバー側での処理とアプリについてお伝えします。

# 参考
https://github.com/hideakitai/ESP32DMASPI/blob/master/ESP32DMASPISlave.h

https://github.com/mobizt/Firebase-ESP-Client